HextaUI

Chat Sidebar

The sidebar for a chat app. Logo, search and New chat up top, your own links below it, pinned chats, projects that expand to show their chats, recents grouped by day, and rows with hover and right-click menus, inline rename, delete with undo and live reply states.

ChatGPT, Claude, Perplexity and Grok all settled on the same sidebar: start a chat or search from the top, keep a few chats pinned, group work into projects, and scroll back through everything else by day. Chat Sidebar is that sidebar, built on the HextaUI Sidebar’s inset variant, so the conversation sits in its own rounded panel inside the frame. It collapses to an icon rail on desktop, and on phones it slides in to fill the screen and slides away as soon as you pick a chat.

The header holds your logo, a search button and the collapse button. New chat sits below with its shortcut always visible, followed by any links you pass, such as Images, Library or Plugins. Projects are collapsible folders, each with its own icon, and their chats sit indented inside them rather than in the history. Recents group into Today, Yesterday, Previous 7 days, Previous 30 days and then by month, and render in pages as you scroll, so thousands of chats open instantly.

Every row has a menu on hover and the same menu on right-click: pin, rename, move to a project and delete. Rename happens in place with double-click or F2. Delete hides the chat at once and offers Undo for five seconds before calling onDelete. A chat that is replying shows a spinner and one that finished elsewhere shows a dot; both step aside for the menu button on hover. The account menu opens upward with your name and email on top of the items you pass in.

  1. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  3. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/chat-sidebar

Wire it to your state

Pass the chats and the handlers you support. Each menu item appears only when its handler is set, so a read-only history needs just onSelect and onNewChat.

"use client"

import * as React from "react"

import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"

import { ChatSidebar } from "@/components/blocks/chat-sidebar/chat-sidebar"
import type { SidebarChat } from "@/components/blocks/chat-sidebar/chats"

export function App({ initialChats }: { initialChats: SidebarChat[] }) {
  const [chats, setChats] = React.useState(initialChats)
  const [activeId, setActiveId] = React.useState<string | null>(null)

  const update = (id: string, change: Partial<SidebarChat>) =>
    setChats((current) => current.map((chat) => (chat.id === id ? { ...chat, ...change } : chat)))

  return (
    <SidebarProvider>
      <ChatSidebar
        chats={chats}
        activeId={activeId}
        onSelect={setActiveId}
        onNewChat={() => setActiveId(null)}
        onRename={(id, title) => update(id, { title })}
        onPinnedChange={(id, pinned) => update(id, { pinned })}
        onDelete={(id) => setChats((current) => current.filter((chat) => chat.id !== id))}
        user={{ name: "Mia Chen", plan: "Pro plan" }}
      />
      <SidebarInset />
    </SidebarProvider>
  )
}

Projects and routing

Each project is a collapsible folder with its own icon. Its chats sit indented inside it instead of in the dated history. onMove adds Move to project to every row.

"use client"

import { useRouter } from "next/navigation"

import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"

import { ChatSidebar } from "@/components/blocks/chat-sidebar/chat-sidebar"
import type { SidebarChat, SidebarProject } from "@/components/blocks/chat-sidebar/chats"

export function Workspace({
  chats,
  projects,
  activeId,
  moveChat,
  children,
}: {
  chats: SidebarChat[]
  projects: SidebarProject[]
  activeId: string | null
  moveChat: (id: string, projectId: string | null) => void
  children: React.ReactNode
}) {
  const router = useRouter()

  return (
    <SidebarProvider>
      <ChatSidebar
        chats={chats}
        projects={projects}
        activeId={activeId}
        onSelect={(id) => router.push(`/c/${id}`)}
        onNewChat={() => router.push("/")}
        defaultOpenProjects={projects.slice(0, 1).map((project) => project.id)}
        onMove={moveChat}
      />
      <SidebarInset>{children}</SidebarInset>
    </SidebarProvider>
  )
}

Anatomy

The parts you compose, from the outside in.

PartDescription
ChatSidebarThe whole sidebar: header, New chat and links, pinned chats, projects, dated history and the account menu.
groupChatsSorts chats newest first and splits them into the dated groups the sidebar shows.
searchChatsWord-based title search used by the search dialog. Returns the 8 most recent chats for an empty query.
PropTypeDefault
chatsEvery chat, in any order. Each has id, title, updatedAt (ms) and optional projectId, pinned and status ("streaming" or "unread").
SidebarChat[]–
projectsProjects with id, name and an optional icon. Each renders as a collapsible folder holding its chats. The section hides when empty.
SidebarProject[][]
activeIdThe open chat, marked as the current page.
string | null–
onSelectCalled when a chat is chosen from the list, search or the recents card in the icon rail.
(id: string) => void–
onNewChatCalled from the New chat button and ⌘⇧O.
() => void–
linksExtra rows under New chat, such as Images, Library or Plugins. They stay as icons with tooltips in the rail.
{ label, icon, href?, active?, onSelect? }[][]
defaultOpenProjectsProjects that start expanded.
string[][]
onProjectOpenChangeCalled when a project is expanded or collapsed, so you can remember it.
(id: string, open: boolean) => void–
onRenameEnables Rename, double-click and F2. Called only when the title actually changed.
(id: string, title: string) => void–
onPinnedChangeEnables Pin and Unpin.
(id: string, pinned: boolean) => void–
onMoveEnables Move to project. null removes the chat from its project.
(id: string, projectId: string | null) => void–
onDeleteEnables Delete. Called after the five-second Undo window closes.
(id: string) => void–
brandYour logo, at the top next to the search and collapse buttons.
ReactNode–
userShows the account button in the footer.
{ name, email?, image?, plan? }–
accountMenuDropdownMenu items under the name and email, such as Settings, a Help submenu and Log out.
ReactNode–
pageSizeHow many history rows render at a time. More load as you near the end of the list.
number60
classNameClasses for the Sidebar root.
string–
KeyAction
⌘KOpens or closes chat search. The search button in the header does the same.
⌘⇧OStarts a new chat.
⌘BCollapses to the icon rail or expands. On phones, opens or closes the full-screen sidebar.
F2Renames the focused chat. Double-click does the same.
EnterExpands or collapses the focused project. While renaming, saves the new title; leaving the field also saves.
EscCancels a rename, or closes a menu or search.
  • The open chat is marked with aria-current="page", and each row’s menu button is named after its chat, such as “Options for Pricing page copy”. Reply states are announced as “Replying” and “Unread reply” rather than shown only as a spinner or dot.
  • Menu buttons appear on hover for mouse users but stay reachable with Tab, and every row action is in that menu, so nothing depends on right-click or double-click. Projects report aria-expanded, and their chats stay findable with the browser’s find once a project has been opened.
  • The typing title and the full-screen slide on phones respect reduced motion. Undo stays in the toast until it times out, and focus stays where it was when a chat is deleted.

Code

3 files, added to components/blocks/chat-sidebar.