Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

AI Kit

Conversation List

AI Kit

Conversation List React component for Tailwind CSS v4

Chat WidgetFluid Orb
Live props
State
defaultValue
defaultSearchValue
searching
loading
skeletonCount8
hasMore
loadingMore
Behavior
search
searchShortcut
filter
confirmDelete
newChatShortcut
locale
Layout
groupBy
Content
showPreview
showTime
showArchived
Appearance
size
radius
tone
color
actionsVisibility
Code
import { ConversationList } from "@/components/ui/conversation-list"

<ConversationList />
  1. Home
  2. Components
  3. AI Kit
  4. Conversation List
Pro

Conversation List React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React chat history list for AI apps: New chat, search, pinned chats and day groups, with rename, pin, archive and delete.

Click or tap a chat to open it: the open one is tinted and keeps its ⋯ button, rows with an href navigate through your router link. Chats sit under sticky day labels (Pinned, Today, Yesterday, Previous 7 days, Previous 30 days, then one per month), newest first; a reply still streaming shows a small spinner and an unread chat a dot. With a mouse the ⋯ appears on hover or focus while the title fades out beneath it; on touch it is always there and the rows are 44px. The ⋯ menu (a bottom sheet on phones) renames, pins or unpins, archives and deletes, below any entries of your own; a long press on a touch screen opens the same menu (the sheet on a phone) with a haptic tick. Rename edits the title in place on a desktop (Enter or a click away saves, Escape cancels, an empty title keeps the old one) and on a phone in a small sheet with Save whose field has the keyboard up at once. Delete asks first, then the chat leaves at once and focus moves to the next one; pin moves a chat into Pinned while the rows around it slide (in one ungrouped list it glides up, lifted over the others). Every change shows right away, stays busy with a spinner while your handler's promise runs, and rolls back with an announcement if it rejects. Typing in the search turns the groups into one ranked list with the matched letters in bold (a match in the last message shows that part of it); Down steps from the search into the results. The rows are one Tab stop: Up and Down move, Home and End jump, Page Up and Page Down skip ten, Enter opens, F2 renames, Delete or Backspace deletes, Shift+F10 or the menu key opens the actions, and Tab reaches the focused row's ⋯. A title cut off or faded under the ⋯ shows in full as the row's tooltip after a moment with a mouse. Load more at the end also runs when it scrolls into view. Reduced motion turns every slide into a short fade.

Conversation List is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add conversation-list, together with Alert Dialog, Button, Drawer, Dropdown Menu, Empty State and 18 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add conversation-list

Conversation List is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import ConversationList from @/components/ui/conversation-list.

import { useState } from "react"
import { Share2 } from "lucide-react"
import { ConversationList, type ConversationItem } from "@/components/ui/conversation-list"

// your own wording over the English defaults (DEFAULT_CONVERSATION_LIST_LABELS)
const LABELS = {
  nav: "Assistant chats",
  pinned: "Starred",
  deleteTitle: (title: string) => `Delete “${title}” for good?`,
}

export function ChatHistory({ initial }: { initial: ConversationItem[] }) {
  const [chats, setChats] = useState(initial)
  const [open, setOpen] = useState(initial[0]?.id ?? "")
  const patch = (id: string, change: Partial<ConversationItem>) =>
    setChats((list) => list.map((chat) => (chat.id === id ? { ...chat, ...change } : chat)))

  return (
    <aside className="h-dvh w-72 bg-card">
      <ConversationList
        conversations={chats}
        value={open}
        onValueChange={setOpen}
        onNewChat={() => {
          const id = crypto.randomUUID()
          setChats((list) => [{ id, title: "", updatedAt: Date.now() }, ...list])
          setOpen(id)
        }}
        onRename={async (id, title) => {
          await fetch(`/api/chats/${id}`, { method: "PATCH", body: JSON.stringify({ title }) })
          patch(id, { title })
        }}
        onPinChange={(id, pinned) => patch(id, { pinned })}
        onArchive={(id) => patch(id, { archived: true })}
        onDelete={(id) => setChats((list) => list.filter((chat) => chat.id !== id))}
        getActions={(chat) => [{ id: "share", label: "Share", icon: <Share2 />, onSelect: () => navigator.clipboard.writeText(`/chat/${chat.id}`) }]}
        newChatShortcut="shift+mod+o"
        showTime
        locale="en-US"
        labels={LABELS}
      />
    </aside>
  )
}

// server search: control the text, show the server's results as they come, and keep them unfiltered
// <ConversationList conversations={results} searchValue={query} onSearchChange={setQuery} searching={pending} filter={false} />
// long histories: hasMore + onLoadMore (a promise keeps Load more spinning; it also runs when it scrolls into view)

Props

  • conversations*
    ConversationItem[]

    The history: { id, title, updatedAt, preview?, pinned?, unread?, streaming?, archived?, href?, icon?, model? }. updatedAt (a Date, an ISO string or ms) sorts and groups them; a missing or invalid one goes to Earlier at the end. An empty title shows labels.untitled. Keep the objects immutable: a new object only for the chat that changed.

  • value
    string

    The open conversation's id (controlled). Without it, rows with an href follow the current page of a LinkProvider (currentPath), and otherwise the last clicked row stays open. An id that is not in the list highlights nothing.

  • defaultValuedefault ""
    string

    The conversation open on the first render while uncontrolled. Empty in the playground opens Overdue invoices, September.

  • onValueChange
    (id: string, item: ConversationItem) => void

    A row was chosen (a click, a tap or Enter). Rows with an href also navigate.

  • onNewChat
    () => void

    Shows the New chat button at the top (and makes newChatShortcut work).

  • onRename
    (id: string, title: string) => (void | Promise<unknown>)

    Enables Rename in the menu, F2 and a double click with a mouse. The new title (trimmed, never empty) shows at once; a returned promise keeps the row busy, and a rejection brings the old title back and announces the error.

  • onPinChange
    (id: string, pinned: boolean) => (void | Promise<unknown>)

    Enables Pin / Unpin. The chat slides into (or out of) Pinned at once and rolls back if the promise rejects.

  • onArchive
    (id: string) => (void | Promise<unknown>)

    Enables Archive. The chat leaves the list at once (unless showArchived) and comes back if the promise rejects.

  • onDelete
    (id: string) => (void | Promise<unknown>)

    Enables Delete (the menu, Delete or Backspace on a focused row). After the confirmation the chat leaves at once and focus moves to the next chat, or New chat; a rejection brings it back.

  • getActions
    (item: ConversationItem) => MenuEntry[]

    Extra dropdown-menu entries above the built-in ones (Share, Move to project, a submenu). Called each time a menu opens, so they can depend on the app's state.

  • searchdefault true
    boolean

    The search field under New chat. Its text still filters the list when you hide it and pass searchValue yourself.

  • searchValue
    string

    The search text (controlled). Pair it with onSearchChange for a server search, then pass the results as conversations and filter={false}.

  • defaultSearchValuedefault ""
    string

    The search text on the first render while uncontrolled.

  • onSearchChange
    (value: string) => void

    Every keystroke in the search, and the clear.

  • searchingdefault false
    boolean

    The search icon turns into a spinner while your server looks for results.

  • searchShortcutdefault ""
    string

    A key combo in mod+k notation that focuses the search from anywhere, shown as a hint in the empty field. "" binds nothing.

  • filterdefault true
    boolean

    Filters titles, previews and model names locally while searching (case and diacritics ignored, best matches first). false keeps conversations as given, for results that come from a server.

  • groupBydefault "date"
    "date" | "none"

    date: Pinned, Today, Yesterday, Previous 7 days, Previous 30 days, then one group per month, each under a sticky label. none: one list, pinned chats first with a pin glyph.

  • showPreviewdefault false
    boolean

    A second line with the last message; rows grow to 52 / 56px.

  • showTimedefault false
    boolean

    A trailing time: "4:35 PM" today, "Yesterday" yesterday, "Sep 22" this year, the full date before. With a mouse the ⋯ takes its place on hover.

  • showArchiveddefault false
    boolean

    Lists archived chats too, muted and with an archive glyph (the demo has one from August).

  • sizedefault "md"
    "sm" | "md"

    Rows 36 / 40px (44px on touch), with New chat and the search at the same height.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    The rows' corners, the button's radius at the row size (8 / 10px at md); full makes pills.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    Colors the open row (a gray tint for neutral, the tone's soft fill otherwise), the unread dot and the streaming spinner.

  • colordefault ""
    string

    Any CSS color. Replaces the tone for the open row, the dot and the spinner. Leave empty to use the tone.

  • actionsVisibilitydefault "hover"
    "hover" | "always"

    hover: with a mouse the ⋯ shows on hover and keyboard focus (it is always there on touch and on the open chat). always: on every row, which keeps room for it.

  • confirmDeletedefault true
    boolean

    Asks in an alert dialog before onDelete. false deletes at once.

  • newChatShortcutdefault ""
    string

    A key combo in mod+k notation that calls onNewChat from anywhere, shown on the button (⇧⌘O on a Mac, Ctrl+Shift+O elsewhere) and hidden on touch. "" binds nothing; the demo uses shift+mod+o.

  • loadingdefault false
    boolean

    Skeleton rows with the real row shape (two lines with showPreview) and aria-busy, while the history loads.

  • skeletonCountdefault 8
    number

    How many skeleton rows loading shows.

  • hasMoredefault false
    boolean

    Load more at the end of the list; it also runs onLoadMore when it scrolls into view (once per new page, never while searching).

  • onLoadMore
    () => (void | Promise<unknown>)

    Loads the next page of older chats; a returned promise keeps Load more spinning until it settles.

  • loadingMoredefault false
    boolean

    Keeps Load more spinning, for apps that track the request themselves.

  • emptyState
    ReactNode

    Replaces the empty state (a chat icon, labels.empty and labels.emptyDescription). A search with no match shows its own, with Clear search.

  • linkComponent
    (props: LinkComponentProps) => ReactNode

    Renders rows that have an href: next/link's Link, react-router's Link. Defaults to the LinkProvider's, then a plain <a>.

  • now
    Date | number

    The reference time for the day groups and times; default useNow(), which ticks each minute. Without it the list shows skeleton rows on the server and during hydration, so pass a server time to render the real list there.

  • localedefault ""
    string

    Formats times, month groups and the result count ("en-US"). Empty uses the UIProvider's locale, then the browser's.

  • labels
    Partial<ConversationListLabels>

    Every visible and spoken string, English by default (DEFAULT_CONVERSATION_LIST_LABELS): group names, the menu, the delete confirmation, the rename sheet, the empty states and the announcements. Counted and titled ones are functions: results(count, formatted), actions(title), deleteTitle(title), renamed(title). Pass a stable object.

  • classNames
    Partial<Record<"header" | "newChat" | "search" | "scroller" | "group" | "groupLabel" | "list" | "item" | "row" | "icon" | "title" | "preview" | "indicator" | "time" | "actions" | "rename" | "more" | "empty" | "skeleton", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root <nav>. It inherits its parent's background so the sticky day labels cover the rows; give it one (bg-card) when the parent is transparent.

Parts

  • ConversationListItem

    component

    One row on its own (a recent-chats card, a search result): item, active, onSelect, size, radius, tone, color, showPreview, showTime, now, locale, linkComponent, labels, classNames. Same look and link behavior, no menu; render it inside a <ul>. Props: ConversationListItemProps.

  • groupConversations

    function

    The grouping the list uses, pure: [{ key: "pinned" | "today" | "yesterday" | "week" | "month" | "2026-08" | "earlier", label, items }], newest first inside each group, month labels like "August 2026". Signature: (items: ConversationItem[], options: { now: Date | number; locale?: string; labels?: Partial<ConversationListLabels> }) => ConversationGroup[].

  • DEFAULT_CONVERSATION_LIST_LABELS

    constant

    The English strings, to spread a translation over. Type: ConversationListLabels.

  • ConversationItem

    type

    One chat: id, title, updatedAt (sorts and groups the list), preview, pinned, unread, streaming, archived, href (renders the row as a link), icon and model.

  • ConversationGroup

    type

    One group from groupConversations: key ("pinned", "today", a month such as "2026-08"...), label and items.

Examples

In the app sidebar

Inside the sidebar under the workspace switcher; rows are router links and the open page is highlighted.

With previews and times

showPreview and showTime: the last message under each title and a trailing 4:35 PM / Yesterday / Sep 22.

Server search

A controlled searchValue, filter={false} and a spinner in the field while the request runs.

Infinite

100 generated chats in pages of 20 with hasMore and onLoadMore; older ones group by month.

Empty and loading

Skeleton rows while the history loads, and an empty state that the first chat replaces.

Phone

A screen of its own at 350px: 44px rows, the ⋯ always visible, a long press opens the action sheet.

Tones and colors

Brand for the open chat and the unread dot; a custom violet with small pill rows and actions always shown.

Recent chats, one row at a time

ConversationListItem in a card of its own: default labels, no menu.

Dependencies

motionlucide-reactalert-dialogbuttondrawerdropdown-menuempty-stateinputkbdsearch-inputskeletonspinneruse-controllable-stateuse-localeuse-long-pressuse-media-queryuse-shortcutdate-utilsmotionnavigationnumber-formatrelative-timetext-matchtonesui-config

Files

  • components/ui/conversation-list.tsx

FAQ

Is the React Conversation List component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Conversation List?

Run npx wingo-ui@latest add conversation-list. It also adds the Wingo UI items it builds on (Alert Dialog, Button, Drawer, Dropdown Menu, Empty State and 18 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/conversation-list.

How does Conversation List behave on phones?

The ⋯ menu (a bottom sheet on phones) renames, pins or unpins, archives and deletes, below any entries of your own; a long press on a touch screen opens the same menu (the sheet on a phone) with a haptic tick.

See also

SidebarListSearch InputDropdown MenuAI Chatlib

Used in

AI Chat Page

More React AI components

Prompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage Meter
All 17 React AI components
PreviousChat WidgetNext Fluid Orb

Pinned

Today

Yesterday

Previous 7 days

Previous 30 days

Overdue invoices, September

Acme Pro

Yes, write the one for Tech Solutions.

Here is a short email for Daniel Brooks (Tech Solutions Inc.). I attached the invoice. Send it now to daniel.brooks@techsolutions.com?

Ask Acme Assistant…

value="invoices" · confirmDelete=true · filter=true · new chat shift+mod+o