Switch between conversations. Supports sidebar or dropdown layouts.
This demo uses ThreadListSidebar, which includes thread-list as a dependency and provides a complete sidebar layout. For custom implementations, you can use thread-list directly.
Getting Started
Add the component
Use threadlist-sidebar for a complete sidebar layout or thread-list for custom layouts.
ThreadListSidebar
npx shadcn@latest add https://r.assistant-ui.com/threadlist-sidebar.jsonMain Component
import * as React from "react";import { Github, MessagesSquare } from "lucide-react";import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail,} from "@/components/ui/sidebar";import { ThreadList } from "@/components/assistant-ui/thread-list";export function ThreadListSidebar({ ...props}: React.ComponentProps<typeof Sidebar>) { return ( <Sidebar {...props}> <SidebarHeader className="aui-sidebar-header mb-2 border-b"> <div className="aui-sidebar-header-content flex items-center justify-between"> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" asChild> <a href="https://assistant-ui.com" target="_blank" rel="noopener noreferrer" > <div className="aui-sidebar-header-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> <MessagesSquare className="aui-sidebar-header-icon size-4" /> </div> <div className="aui-sidebar-header-heading mr-6 flex flex-col gap-0.5 leading-none"> <span className="aui-sidebar-header-title font-semibold"> assistant-ui </span> </div> </a> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </div> </SidebarHeader> <SidebarContent className="aui-sidebar-content px-2"> <ThreadList /> </SidebarContent> <SidebarRail /> <SidebarFooter className="aui-sidebar-footer border-t"> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton size="lg" asChild> <a href="https://github.com/assistant-ui/assistant-ui" target="_blank" rel="noopener noreferrer" > <div className="aui-sidebar-footer-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> <Github className="aui-sidebar-footer-icon size-4" /> </div> <div className="aui-sidebar-footer-heading flex flex-col gap-0.5 leading-none"> <span className="aui-sidebar-footer-title font-semibold"> GitHub </span> <span>View Source</span> </div> </a> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarFooter> </Sidebar> );}assistant-ui dependencies
npm install @assistant-ui/react radix-uiimport { Button } from "@/components/ui/button";import { Skeleton } from "@/components/ui/skeleton";import { AuiIf, ThreadListItemMorePrimitive, ThreadListItemPrimitive, ThreadListPrimitive,} from "@assistant-ui/react";import { ArchiveIcon, MoreHorizontalIcon, PlusIcon, TrashIcon,} from "lucide-react";import type { FC } from "react";export const ThreadList: FC = () => { return ( <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col gap-1"> <ThreadListNew /> <AuiIf condition={(s) => s.threads.isLoading}> <ThreadListSkeleton /> </AuiIf> <AuiIf condition={(s) => !s.threads.isLoading}> <ThreadListPrimitive.Items components={{ ThreadListItem }} /> </AuiIf> </ThreadListPrimitive.Root> );};const ThreadListNew: FC = () => { return ( <ThreadListPrimitive.New asChild> <Button variant="outline" className="aui-thread-list-new h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted" > <PlusIcon className="size-4" /> New Thread </Button> </ThreadListPrimitive.New> );};const ThreadListSkeleton: FC = () => { return ( <div className="flex flex-col gap-1"> {Array.from({ length: 5 }, (_, i) => ( <div key={i} role="status" aria-label="Loading threads" className="aui-thread-list-skeleton-wrapper flex h-9 items-center px-3" > <Skeleton className="aui-thread-list-skeleton h-4 w-full" /> </div> ))} </div> );};const ThreadListItem: FC = () => { return ( <ThreadListItemPrimitive.Root className="aui-thread-list-item group flex h-9 items-center gap-2 rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted"> <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center px-3 text-start text-sm"> <span className="aui-thread-list-item-title min-w-0 flex-1 truncate"> <ThreadListItemPrimitive.Title fallback="New Chat" /> </span> </ThreadListItemPrimitive.Trigger> <ThreadListItemMore /> </ThreadListItemPrimitive.Root> );};const ThreadListItemMore: FC = () => { return ( <ThreadListItemMorePrimitive.Root> <ThreadListItemMorePrimitive.Trigger asChild> <Button variant="ghost" size="icon" className="aui-thread-list-item-more mr-2 size-7 p-0 opacity-0 transition-opacity group-hover:opacity-100 data-[state=open]:bg-accent data-[state=open]:opacity-100 group-data-active:opacity-100" > <MoreHorizontalIcon className="size-4" /> <span className="sr-only">More options</span> </Button> </ThreadListItemMorePrimitive.Trigger> <ThreadListItemMorePrimitive.Content side="bottom" align="start" className="aui-thread-list-item-more-content z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md" > <ThreadListItemPrimitive.Archive asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"> <ArchiveIcon className="size-4" /> Archive </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Archive> <ThreadListItemPrimitive.Delete asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-destructive text-sm outline-none hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive"> <TrashIcon className="size-4" /> Delete </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Delete> </ThreadListItemMorePrimitive.Content> </ThreadListItemMorePrimitive.Root> );};"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slot } from "radix-ui";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & { tooltip: string; side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn("aui-button-icon size-6 p-1", className)} ref={ref} > <Slot.Slottable>{children}</Slot.Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> );});TooltipIconButton.displayName = "TooltipIconButton";ThreadList
npx shadcn@latest add https://r.assistant-ui.com/thread-list.jsonMain Component
npm install @assistant-ui/reactimport { Button } from "@/components/ui/button";import { Skeleton } from "@/components/ui/skeleton";import { AuiIf, ThreadListItemMorePrimitive, ThreadListItemPrimitive, ThreadListPrimitive,} from "@assistant-ui/react";import { ArchiveIcon, MoreHorizontalIcon, PlusIcon, TrashIcon,} from "lucide-react";import type { FC } from "react";export const ThreadList: FC = () => { return ( <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col gap-1"> <ThreadListNew /> <AuiIf condition={(s) => s.threads.isLoading}> <ThreadListSkeleton /> </AuiIf> <AuiIf condition={(s) => !s.threads.isLoading}> <ThreadListPrimitive.Items components={{ ThreadListItem }} /> </AuiIf> </ThreadListPrimitive.Root> );};const ThreadListNew: FC = () => { return ( <ThreadListPrimitive.New asChild> <Button variant="outline" className="aui-thread-list-new h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted" > <PlusIcon className="size-4" /> New Thread </Button> </ThreadListPrimitive.New> );};const ThreadListSkeleton: FC = () => { return ( <div className="flex flex-col gap-1"> {Array.from({ length: 5 }, (_, i) => ( <div key={i} role="status" aria-label="Loading threads" className="aui-thread-list-skeleton-wrapper flex h-9 items-center px-3" > <Skeleton className="aui-thread-list-skeleton h-4 w-full" /> </div> ))} </div> );};const ThreadListItem: FC = () => { return ( <ThreadListItemPrimitive.Root className="aui-thread-list-item group flex h-9 items-center gap-2 rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted"> <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center px-3 text-start text-sm"> <span className="aui-thread-list-item-title min-w-0 flex-1 truncate"> <ThreadListItemPrimitive.Title fallback="New Chat" /> </span> </ThreadListItemPrimitive.Trigger> <ThreadListItemMore /> </ThreadListItemPrimitive.Root> );};const ThreadListItemMore: FC = () => { return ( <ThreadListItemMorePrimitive.Root> <ThreadListItemMorePrimitive.Trigger asChild> <Button variant="ghost" size="icon" className="aui-thread-list-item-more mr-2 size-7 p-0 opacity-0 transition-opacity group-hover:opacity-100 data-[state=open]:bg-accent data-[state=open]:opacity-100 group-data-active:opacity-100" > <MoreHorizontalIcon className="size-4" /> <span className="sr-only">More options</span> </Button> </ThreadListItemMorePrimitive.Trigger> <ThreadListItemMorePrimitive.Content side="bottom" align="start" className="aui-thread-list-item-more-content z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md" > <ThreadListItemPrimitive.Archive asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"> <ArchiveIcon className="size-4" /> Archive </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Archive> <ThreadListItemPrimitive.Delete asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-destructive text-sm outline-none hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive"> <TrashIcon className="size-4" /> Delete </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Delete> </ThreadListItemMorePrimitive.Content> </ThreadListItemMorePrimitive.Root> );};assistant-ui dependencies
npm install radix-ui"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slot } from "radix-ui";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & { tooltip: string; side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn("aui-button-icon size-6 p-1", className)} ref={ref} > <Slot.Slottable>{children}</Slot.Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> );});TooltipIconButton.displayName = "TooltipIconButton";Use in your application
import { Thread } from "@/components/assistant-ui/thread";
import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar";
import {
SidebarProvider,
SidebarInset,
SidebarTrigger
} from "@/components/ui/sidebar";
export default function Assistant() {
return (
<SidebarProvider>
<div className="flex h-dvh w-full">
<ThreadListSidebar />
<SidebarInset>
{/* Add sidebar trigger, location can be customized */}
<SidebarTrigger className="absolute top-4 left-4" />
<Thread />
</SidebarInset>
</div>
</SidebarProvider>
);
}import { Thread } from "@/components/assistant-ui/thread";
import { ThreadList } from "@/components/assistant-ui/thread-list";
export default function Assistant() {
return (
<div className="grid h-full grid-cols-[200px_1fr]">
<ThreadList />
<Thread />
</div>
);
}Anatomy
The ThreadList component is built with the following primitives:
import { ThreadListPrimitive, ThreadListItemPrimitive } from "@assistant-ui/react";
<ThreadListPrimitive.Root>
<ThreadListPrimitive.New />
<ThreadListPrimitive.Items
components={{
ThreadListItem: () => (
<ThreadListItemPrimitive.Root>
<ThreadListItemPrimitive.Trigger>
<ThreadListItemPrimitive.Title />
</ThreadListItemPrimitive.Trigger>
<ThreadListItemPrimitive.Archive />
<ThreadListItemPrimitive.Delete />
</ThreadListItemPrimitive.Root>
),
}}
/>
</ThreadListPrimitive.Root>API Reference
ThreadListPrimitive.Root
Container for the thread list.
ThreadListPrimitiveRootPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
ThreadListPrimitive.Items
Renders all threads in the list.
ThreadListPrimitiveItemsPropsarchived?: booleanWhen true, renders archived threads instead of active threads.
componentsrequired: objectComponent configuration.
ComponentsThreadListItemrequired: ComponentTypeComponent to render for each thread item.
ThreadListPrimitive.New
A button to create a new thread.
ThreadListPrimitiveNewPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemPrimitive.Root
Container for a single thread item. Automatically sets data-active and aria-current when this is the current thread.
ThreadListItemPrimitiveRootPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
ThreadListItemPrimitive.Trigger
A button that switches to this thread when clicked.
ThreadListItemPrimitiveTriggerPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemPrimitive.Title
Renders the thread's title.
ThreadListItemPrimitiveTitlePropsfallback?: ReactNodeContent to display when the thread has no title.
ThreadListItemPrimitive.Archive
A button to archive the thread.
ThreadListItemPrimitiveArchivePropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemPrimitive.Unarchive
A button to restore an archived thread.
ThreadListItemPrimitiveUnarchivePropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemPrimitive.Delete
A button to permanently delete the thread.
ThreadListItemPrimitiveDeletePropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemMorePrimitive
A dropdown menu for additional thread actions, built on Radix UI DropdownMenu.
ThreadListItemMorePrimitive.Root
Menu container that manages dropdown state.
ThreadListItemMorePrimitiveRootPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
ThreadListItemMorePrimitive.Trigger
Button to open the menu.
ThreadListItemMorePrimitiveTriggerPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper button.
ThreadListItemMorePrimitive.Content
Menu content container.
ThreadListItemMorePrimitiveContentPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
ThreadListItemMorePrimitive.Item
Individual menu item.
ThreadListItemMorePrimitiveItemPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
ThreadListItemMorePrimitive.Separator
Visual separator between items.
ThreadListItemMorePrimitiveSeparatorPropsasChild: boolean= falseMerge props with child element instead of rendering a wrapper div.
Related Components
- Thread - The main chat interface displayed alongside the list