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

Chat Thread

AI Kit

Chat Thread React component for Tailwind CSS v4

Chat MessageChat Widget
Live props
State
thinking
hasMore
loadingMore
loading
Content
greeting
emptyDescription
Appearance
variant
size
dateSeparators
Layout
maxWidth
Behavior
stickToBottom
jumpButton
announce
locale
Code
import { ChatThread } from "@/components/ui/chat-thread"

<ChatThread />
  1. Home
  2. Components
  3. AI Kit
  4. Chat Thread
Pro

Chat Thread React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React chat thread for AI chats: the scrolling conversation, pinned to the newest message while it streams, with day pills and follow-ups.

The thread opens at the newest message and follows a reply while it streams. Scroll up (wheel, finger, arrows, Page Up) and it lets go at once; a pill floats above the composer with the number of new replies, and tapping it glides back down and puts focus on the newest message. Sending always brings you to the bottom. Day pills stick to the top while their day scrolls under them. Reach the top of a long history and older messages load above without the view moving. Under the latest answer, follow-up rows send themselves on a tap or Enter (arrow keys move between them). An empty thread shows a greeting with starter cards that scroll sideways on phones. Tab reaches the header, the message list (it scrolls with the keyboard), each message's actions, the follow-ups, the jump pill and the composer. Screen readers hear once when a reply starts and when it is ready, never every streamed word.

Chat Thread 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 chat-thread, together with Chat Message, Suggestion Chips, Markdown, Button, Skeleton and 9 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 chat-thread

Chat Thread 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 ChatThread from @/components/ui/chat-thread.

import { useState } from "react"
import { ChatThread, type ChatMessageData, type ChatThreadLabels } from "@/components/ui/chat-thread"
import { PromptInput } from "@/components/ui/prompt-input"
import { createChatId, mockStream } from "@/lib/ai-chat"

const labels: Partial<ChatThreadLabels> = {
  greeting: "Hi, Anna! How can I help today?",
  jump: "Back to the latest message",
}

export function Assistant() {
  const [messages, setMessages] = useState<ChatMessageData[]>([])
  const [thinking, setThinking] = useState(false)

  async function send(text: string) {
    const id = createChatId("a")
    setMessages((list) => [...list, { id: createChatId("u"), role: "user", content: text, createdAt: Date.now() }])
    setThinking(true)
    let content = ""
    for await (const part of mockStream("I found **3 overdue invoices**, $22,775.95 in total.")) {
      if (!content) setThinking(false)
      content += part
      const snapshot = content
      // a new object only for the message that changed
      setMessages((list) =>
        list.some((m) => m.id === id)
          ? list.map((m) => (m.id === id ? { ...m, content: snapshot } : m))
          : [...list, { id, role: "assistant", content: snapshot, status: "streaming", createdAt: Date.now() }],
      )
    }
    setMessages((list) => list.map((m) => (m.id === id ? { ...m, status: "complete" } : m)))
  }

  return (
    <div className="h-dvh">
      <ChatThread
        messages={messages}
        thinking={thinking}
        labels={labels}
        composer={<PromptInput onSubmit={(message) => send(message.text)} />}
      />
    </div>
  )
}

Props

  • messages*
    ChatMessageData[]

    The conversation, oldest first. Keep message objects immutable: give only the message that changed a new object, so only it re-renders while text streams. Repeated ids are skipped.

  • thinkingdefault false
    boolean

    Shows the pending assistant row (the mark and three dots) after sending and before the first token, unless the last message is already a pending or streaming answer.

  • composer
    ReactNode

    Usually a PromptInput. The thread pins it under the reading column above the safe area, with a fade over the messages behind it. Leave it out for a read-only thread.

  • header
    ReactNode

    A bar above the scroller: the title, a model picker, share.

  • emptyState
    ReactNode

    Replaces the greeting of an empty thread entirely.

  • greetingdefault ""
    ReactNode

    The empty state's title. Empty uses labels.greeting.

  • emptyDescriptiondefault ""
    ReactNode

    A line under the greeting.

  • emptyIcon
    ReactNode

    The empty state's icon. An AI mark by default; a FluidOrb or a logo.

  • suggestions
    ChatSuggestion[]

    Starter cards in the empty state: a grid on desktop, a rail on phones.

  • followUps
    ChatSuggestion[]

    Rows under the latest answer, shown once it is complete and nothing is thinking.

  • onSuggestionSelect
    (suggestion: ChatSuggestion) => void

    A starter card or a follow-up was chosen; send suggestion.prompt ?? suggestion.label.

  • onRegenerate
    ((message: ChatMessageData) => void | Promise<unknown>)

    Regenerate on an assistant message. A returned promise keeps the button loading.

  • onEdit
    ((message: ChatMessageData, content: string) => void | Promise<unknown>)

    A user message was edited in place and sent again.

  • onFeedbackChange
    ((message: ChatMessageData, feedback: ChatFeedback) => void | Promise<unknown>)

    The thumbs on assistant messages. Store it in the message's feedback to control it; a rejected promise rolls back.

  • onRetry
    ((message: ChatMessageData) => void | Promise<unknown>)

    Retry on a message with status error.

  • onCopy
    (message: ChatMessageData, text: string) => void

    After a message was copied (copying is built in).

  • onBranchChange
    (message: ChatMessageData, index: number) => void

    The version switcher of a message with branch.count above 1.

  • messageProps
    Partial<ChatMessageProps>

    Presentation props for every message: bubble, tone, color, showAvatar, assistantAvatar, timestamp, menu, labels, markdownProps. Keep it memoized (a module constant or useMemo) so finished messages never re-render.

  • renderMessage
    (message: ChatMessageData, props: ChatMessageProps) => ReactNode

    Full control over a message (a custom card for some). Return undefined for the default ChatMessage.

  • variantdefault "default"
    "default" | "bubbles" | "flat"

    The messages' layout. default: your bubble on the right, the answer as text on the left. bubbles: both in bubbles (support widgets). flat: both on the left with avatars and names (team threads). messageProps.variant wins.

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

    Message text, avatars and gaps. sm fits copilot side panels.

  • maxWidthdefault "md"
    "sm" | "md" | "lg" | "full"

    The reading column of the messages and the composer: 640 / 768 / 960px or the full width.

  • dateSeparatorsdefault true
    boolean

    A day pill between days ("Yesterday", "Today", "Monday", "Sep 22, 2026") that sticks to the top while its day scrolls. Messages without createdAt get none.

  • stickToBottomdefault true
    boolean

    Follows new content while the reader is at the bottom, and lets go as soon as they scroll up.

  • jumpButtondefault true
    boolean

    The pill that shows while scrolled up, with the number of replies that arrived since.

  • hasMoredefault false
    boolean

    Older messages exist: a sentinel at the top calls onLoadMore when it comes within 400px, once per cycle.

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

    Prepend older messages. The first visible message stays exactly where it was.

  • loadingMoredefault false
    boolean

    A spinner at the top while older messages load.

  • loadingdefault false
    boolean

    The first load: three skeleton message pairs in place of an empty thread.

  • announcedefault "notice"
    "notice" | "full" | "off"

    The polite live region. notice: says when a reply starts and when it is ready. full: also reads the first 600 characters of the reply. The thread itself is never a live region.

  • now
    Date | number

    The reference time for day labels and message times (default: the current minute).

  • localedefault ""
    string

    Day labels, dates and counts. Empty uses the app's locale.

  • labels
    Partial<ChatThreadLabels>

    Every string: conversation, messages, greeting, jump, newMessages(count, formatted), loadMore, loadingMore, loading, thinking, responseStarted, responseReady, responseFailed, today and yesterday (empty uses the locale's word), suggestions, followUps. English by default (DEFAULT_CHAT_THREAD_LABELS). Message strings go in messageProps.labels.

  • classNames
    Partial<Record<"header" | "scroller" | "content" | "list" | "item" | "separator" | "loadMore" | "thinking" | "followUps" | "empty" | "jump" | "composer", string>>

    Extra classes for the inner parts. On a surface other than the page, set the composer fade with className="[--chat-thread-fade:var(--popover)]".

Parts

  • ChatThreadSeparator

    component

    The day pill that sticks to the top of the scroller while its day scrolls under it, with the full date as an sr-only heading.

    • label*
      ReactNode

      The visible pill: "Today", "Yesterday", "Monday" or a date.

    • date
      TimeInput

      The day; its full date becomes the heading ("Thursday, September 24, 2026").

    • locale
      string

      Intl locale for the full date.

    • classNames
      Partial<Record<"pill" | "heading", string>>

      Extra classes for the inner parts.

  • ChatThreadEmpty

    component

    The greeting of a new conversation: an icon, a title, a line under it and starter cards.

    • icon
      ReactNode

      An AI mark by default; a FluidOrb or your logo.

    • greetingdefault "How can I help?"
      ReactNode

      The title.

    • description
      ReactNode

      The line under the title.

    • suggestions
      ChatSuggestion[]

      Starter cards.

    • onSuggestionSelect
      (suggestion: ChatSuggestion) => void

      Runs when a starter card is picked.

    • columns
      SuggestionChipsColumns

      Card grid columns from 640px; below that the cards scroll in a rail.

    • suggestionsLabel
      string

      The starter cards' group name.

  • ChatThreadJump

    component

    The floating pill that brings the reader back to the newest message, with the count of messages that arrived meanwhile.

    • count
      number

      Messages that arrived while the reader was scrolled up; 0 shows the arrow alone.

    • locale
      string

      Intl locale for the count.

    • labels
      Partial<Pick<ChatThreadLabels, "jump" | "newMessages">>

      The pill's text and its count sentence.

  • DEFAULT_CHAT_THREAD_LABELS

    constant

    The English strings: conversation, messages, greeting ("How can I help?"), jump ("Jump to latest"), newMessages, the loading and response announcements and the suggestion group names. today and yesterday are empty, so the day pills use the locale's words. Type: ChatThreadLabels.

Examples

Full app

Conversation history, the thread, a model picker and usage in the composer.

Empty state

A greeting over a FluidOrb with six starter cards.

Copilot panel

A compact thread in a side panel next to a client page.

Support widget

Both sides in bubbles, the person's in brand orange.

Long history

200 messages; older ones load at the top without the view jumping.

Streaming stress

A 3,000-word answer with a counter of finished-message re-renders.

Dependencies

motionlucide-reactchat-messagesuggestion-chipsmarkdownbuttonskeletonspinneruse-stick-to-bottomuse-localeai-chatdate-utilsnumber-formatrelative-timemotionui-config

Files

  • components/ui/chat-thread.tsx

FAQ

Is the React Chat Thread component free?

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

How do I install Chat Thread?

Run npx wingo-ui@latest add chat-thread. It also adds the Wingo UI items it builds on (Chat Message, Suggestion Chips, Markdown, Button, Skeleton and 9 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/chat-thread.

How does Chat Thread behave on phones?

An empty thread shows a greeting with starter cards that scroll sideways on phones.

See also

Chat MessageSuggestion ChipsPrompt InputConversation ListuseStickToBottomhookAI Chatlib

Used in

AI Chat Page

More React AI components

Chat WidgetReasoning IndicatorRule BuilderTool Call TimelineUsage MeterFluid Orb
All 17 React AI components

Articles using this component

  • AI Elements Alternative: 5 React AI Chat Kits Compared

    Building UI with AI agents · Oct 16, 2026 · 11 min read

  • React AI Chat UI: Streaming, Auto-Scroll and Mobile

    Building UI with AI agents · Oct 9, 2026 · 10 min read

PreviousChat MessageNext Chat Widget

Overdue invoices in September

Wednesday, September 23, 2026

Yesterday
  1. You said, 6:10 PM

    Hi! Can you help me with collections tomorrow?

    6:10 PM
  2. Summit Assistant said, 6:10 PM

    Sure. I can search invoices, prepare reminders and build collection reports. Message me when you are ready.

    6:10 PM·Summit Standard

Thursday, September 24, 2026

Today
  1. You said, 4:32 PM

    Which invoices from September are overdue? Put them in a table and suggest what to do next.

    4:32 PM
  2. Summit Assistant said, 4:32 PM

    I found 3 overdue invoices in September, $22,775.95 in total:

    ClientInvoiceAmountLate
    Northwind Tech Inc.#2026-0405$12,480.001 day
    Greenleaf Pharmacy#2026-0371$7,315.2053 days
    Westside Books#2026-0366$2,980.7557 days

    What I suggest:

    1. A polite reminder to Northwind Tech Inc., the due date passed yesterday.
    2. A call to Greenleaf Pharmacy: Daniel Cross, (503) 555-0118.
    3. For Westside Books, a formal payment notice. I can use the Final notice template.

    Should I draft the emails?

    4:32 PM·Summit Pro
Shift + Enter for a new line

last action: — · announce: notice · 4 messages