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

Data Display

Comment Thread

Data Display

Comment Thread React component for Tailwind CSS v4

Code BlockContact card
Live props
Content
placeholder
Appearance
variant
size
tone
color
timeFormat
Layout
showHeader
composer
stickyComposer
State
sort
collapseRepliesAfter2
loading
skeletonCount3
Behavior
confirmDelete
sendOnEnter
locale
Code
import { CommentThread } from "@/components/ui/comment-thread"

<CommentThread />
  1. Home
  2. Components
  3. Data Display
  4. Comment Thread
Pro

Comment Thread React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React comment thread for records: replies, reactions, edit and delete, @mentions and a composer that sends optimistically with retry.

Write in the composer at the bottom and send with the arrow or Cmd/Ctrl+Enter (Enter adds a line; with sendOnEnter, Enter sends and Shift+Enter adds a line). The comment appears at once at 60% opacity with "Sending…", then fades to its time; if the request fails it shakes once and offers Retry and Discard. Type @ and a few letters for the people list: Up and Down move, Enter or Tab insert "@Anna Price ", Escape closes it. Press Up in the empty composer to edit your last comment. Reply opens an inline composer under the thread (Escape cancels, replying to a reply prefills the @name); Edit turns the text into a box with Save and Cancel (Cmd/Ctrl+Enter saves, Escape cancels, an empty text cannot be saved). Tap a reaction pill to toggle yours; the smiley opens the emoji row. The ⋯ menu has Edit, Copy text, Copy link and Delete, which asks first. "Show N more replies" unfolds older replies, the sort button flips oldest and newest first. On a phone every action is visible with a 44px target, the menu and the emoji row open as bottom sheets, and a long press on a comment opens its actions sheet.

Comment Thread is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add comment-thread, together with Alert Dialog, Avatar, Badge, Button, Dropdown Menu and 15 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 comment-thread

Comment 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 CommentThread from @/components/ui/comment-thread.

"use client"

import { useState } from "react"
import { CommentThread, type CommentAuthor, type CommentData } from "@/components/ui/comment-thread"

const anna: CommentAuthor = { id: "anna", name: "Anna Price", avatar: "/demo/ana-pop.jpg" }
const michael: CommentAuthor = { id: "michael", name: "Michael Hayes", avatar: "/demo/mihai-ionescu.jpg", badge: "Author" }

export function QuoteDiscussion() {
  const [comments, setComments] = useState<CommentData[]>([
    {
      id: "c1",
      author: michael,
      body: "Sent the revised quote with 5% off the annual plan. @Anna Price can you take a look?",
      createdAt: "2026-09-24T14:05:00",
      reactions: [{ emoji: "👍", count: 3, reacted: false, users: ["Jessica Dunn", "Ryan Martin", "Liam Brooks"] }],
    },
  ])

  const send = async (body: string, { parentId }: { parentId?: string }) => {
    // await api.post("/quotes/Q-2026-118/comments", { body, parentId })
    const saved: CommentData = { id: crypto.randomUUID(), author: anna, body, createdAt: new Date() }
    setComments((list) =>
      parentId
        ? list.map((c) => (c.id === parentId ? { ...c, replies: [...(c.replies ?? []), saved] } : c))
        : [...list, saved],
    )
    return saved.id
  }

  return (
    <CommentThread
      comments={comments}
      currentUser={anna}
      people={[anna, michael]}
      onSubmit={send}
      onEdit={(id, body) => setComments((list) => list.map((c) => (c.id === id ? { ...c, body, editedAt: new Date() } : c)))}
      onDelete={(id) => setComments((list) => list.map((c) => (c.id === id ? { ...c, deleted: true } : c)))}
      labels={{ title: "Discussion", empty: "No comments yet" }}
      placeholder="Write a comment…"
    />
  )
}

Props

  • commentsdefault "[]"
    CommentData[]

    { id, author: { id, name, avatar?, badge? }, body, createdAt, editedAt?, replies?, reactions?: { emoji, count, reacted, users? }[], attachments?, pending?, failed?, deleted? }. Bodies are plain text; replies nest one level (deeper ones are flattened with the @name in front).

  • currentUser
    CommentAuthor

    You. Turns on the composer, Reply, reactions and the actions on your own comments. Without it the thread is read-only.

  • peopledefault "[]"
    CommentAuthor[]

    Who can be mentioned: @Name in bodies becomes a chip (yours is highlighted) and the composer suggests them after @.

  • reactionsdefault "["👍", "❤️", "😂", "🎉", "😮", "🙏"]"
    string[]

    The emoji row of the reaction picker.

  • placeholderdefault "Write a comment…"
    string

    The empty composer's hint. Replies use labels.replyPlaceholder(name).

  • variantdefault "plain"
    "plain" | "bubble"

    plain: flat comments with a thread line to the replies, like GitHub. bubble: the name and text in a bubble, your own bubbles tinted with the tone, time and actions underneath.

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

    Avatars 28 / 36px (replies 24 / 28px), text 13 / 14px. sm fits side panels.

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

    Your own bubbles, your reactions, the send and save buttons.

  • colordefault ""
    string

    Any CSS color; replaces the tone.

  • timeFormatdefault "relative"
    "relative" | "time" | "date" | "datetime"

    "5 min. ago", "4:40 PM", "9/24/2026" or both. The full date is always in the tooltip.

  • showHeaderdefault true
    boolean

    The title row with the count and the sort button.

  • composerdefault "bottom"
    "bottom" | "top" | "none"

    Where the composer sits; top suits newest-first feeds. Needs currentUser and onSubmit.

  • stickyComposerdefault false
    boolean

    The composer sticks to the edge of the scroll container on a glass background and pads the safe area, for chat-like pages on phones. Set --comment-thread-surface to the background behind it and --comment-thread-bleed to reach the container's edges.

  • sortdefault "oldest"
    "oldest" | "newest"

    Order of the top-level comments; replies are always oldest first. The header button flips it (onSortChange).

  • collapseRepliesAfterdefault 2
    number

    Shows the newest N replies and folds the older ones behind "Show N more replies".

  • loadingdefault false
    boolean

    Skeleton comments with the real shape.

  • skeletonCountdefault 3
    number

    How many skeleton comments show while loading.

  • confirmDeletedefault true
    boolean

    Delete asks first in an alert dialog (a returned promise keeps it loading).

  • sendOnEnterdefault false
    boolean

    Chat style: Enter sends and Shift+Enter adds a line (touch keyboards keep Enter for new lines and show a Send key). By default Enter adds a line and Cmd/Ctrl+Enter sends.

  • localedefault ""
    string

    Locale for the times. Empty uses the UIProvider locale, then the browser's.

  • onSubmit
    (body: string, context: { parentId?: string; mentions: string[] }) => (void | Promise<unknown>)

    A new comment or reply. The optimistic row shows at once; a promise keeps it "Sending…" and a rejection marks it failed with Retry. Resolve with the saved comment's id so the row stays in place when it arrives in comments.

  • onEdit
    (id: string, body: string) => (void | Promise<unknown>)

    Saves an edit. A promise keeps Save loading; a rejection keeps the editor open with an error.

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

    Deletes a comment after the confirmation. Mark it deleted to keep the placeholder above its replies.

  • onReact
    (id: string, emoji: string) => void

    Toggles the current user's reaction.

  • onRetry
    (id: string) => void

    Retry on a comment you marked failed yourself. Optimistic comments from onSubmit retry on their own.

  • onSortChange
    (sort: CommentThreadSort) => void

    The header sort button was pressed.

  • canEdit
    (comment: CommentData) => boolean

    Default: your own comments (needs onEdit).

  • canDelete
    (comment: CommentData) => boolean

    Default: your own comments (needs onDelete). Return true for moderators.

  • getLink
    (comment: CommentData) => string

    What Copy link copies. Default: the page URL with #comment-<id> (every comment has that id).

  • emptyState
    ReactNode

    Replaces the default <EmptyState size="sm" icon={<MessageSquare />} title={labels.empty} />.

  • composerProps
    Partial<CommentComposerProps>

    Extra props for the main composer: attachments, maxLength, autoFocus, submitLabel.

  • labels
    Partial<CommentThreadLabels>

    Every string, English by default (DEFAULT_COMMENT_THREAD_LABELS): title, reply, edit, delete, edited, save, cancel, send, sending, retry, discard, failed, deleted, showReplies(n), hideReplies, react, reactedBy(names, count), empty, deleteTitle, deleteConfirm, sortOldest, sortNewest, mention, actions, copyText, copyLink, newComment(name), replyPlaceholder(name)…

  • classNames
    Partial<Record<CommentThreadSlot, string>>

    header, title, count, sort, list, comment, avatar, main, author, time, body, bubble, attachments, reactions, footer, replies, composer.

Parts

  • CommentItem

    component

    One comment with its replies. Inside a CommentThread it gets the thread's actions; on its own it renders read-only. Props: { comment: CommentData; parentId?: string } & ComponentProps<"li">.

  • CommentComposer

    component

    The composer on its own (a chat input, a review box): auto-growing box, @mention suggestions, Cmd/Ctrl+Enter or Enter to send, a counter with maxLength. A returned promise keeps the send button busy and the text until it resolves. The mention list opens above the box (suggestionsSide="bottom" for a composer at the top; the thread sets it for composer="top"). Props: { value?, defaultValue?, onValueChange?, onSubmit?(body, { mentions }), people?, user?, placeholder?, submitLabel?, autoFocus?, maxLength?, sendOnEnter?, attachments?, disabled?, size?, tone?, color?, onCancel?, onEditLast?, suggestionsSide?: "top" | "bottom", labels?, classNames? }.

  • CommentBody

    component

    The plain-text renderer: line breaks kept, @mentions as chips, URLs as links (new tab, noopener). Never renders HTML. Props: { body: string; people?: CommentAuthor[]; currentUserId?: string }.

  • parseMentions

    function

    Splits text into text, mention and link segments. Signature: (text: string, people: CommentAuthor[]) => CommentSegment[].

  • getMentionIds

    function

    The ids of the people mentioned in a text, each once, in order. The composer passes them to onSubmit as mentions, so you can notify those people. Signature: (text: string, people: CommentAuthor[]) => string[].

  • DEFAULT_COMMENT_THREAD_LABELS

    constant

    Every English string: title ("Comments"), the action names, showReplies(n), reactedBy(names, count), the empty state, the delete dialog, sort labels, the copy menu and the composer placeholders. Spread it into labels to translate a few. Type: CommentThreadLabels.

  • CommentData

    type

    One comment: { id, author, body, createdAt, editedAt?, replies?, reactions?, attachments?, pending?, failed?, deleted? }. Bodies are plain text with @Name mentions.

  • CommentAuthor

    type

    A person who writes or is mentioned: { id, name, avatar?, badge? }.

Examples

Chat on a phone

Bubbles, a sticky glass composer with the safe area, Enter to send.

Mentions

@ opens the team list; @Name tokens in bodies become chips.

Long threads fold

Older replies hide behind “Show 5 more replies”.

Read-only

Without currentUser the thread only shows: no composer, no actions.

Small, in a side panel

size="sm" next to an overdue invoice.

Brand bubbles, newest first

Your bubbles in the tone; the composer on top for feeds.

Empty thread

The empty state, an attach button and a 500 character limit.

Dependencies

motionlucide-reactalert-dialogavatarbadgebuttondropdown-menuempty-statekbdlistboxpopoverskeletontextareause-clipboarduse-controllable-stateuse-localeuse-long-pressmotionrelative-timetext-matchtonesui-config

Files

  • components/ui/comment-thread.tsx

FAQ

Is the React Comment Thread component free?

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

How do I install Comment Thread?

Run npx wingo-ui@latest add comment-thread. It also adds the Wingo UI items it builds on (Alert Dialog, Avatar, Badge, Button, Dropdown Menu and 15 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/comment-thread.

How does Comment Thread behave on phones?

On a phone every action is visible with a 44px target, the menu and the emoji row open as bottom sheets, and a long press on a comment opens its actions sheet.

See also

TimelineTextareaAvatarEmoji ReactionRelative timelib

Used in

CRM Contact Page

More React data display components

Contact cardDescription ListDiff ViewerFile ListFolderGantt
All 23 React data display components

Articles using this component

  • React Emoji Picker: Search, Skin Tones and Phone Sheets

    Component guides · Nov 6, 2026 · 9 min read

  • React Mentions: @People and #Tags That Survive Renames

    Component guides · Oct 19, 2026 · 9 min read

PreviousCode BlockNext Contact card
Quote #Q-2026-118Northwind Tech Inc.
Negotiating

Comments6

  1. Michael HayesAuthor09/24/2026, 2:05 PM
    Sent the revised quote with 5% off the annual plan. @Anna Price can you take a look?
    1. Ryan Martin09/24/2026, 2:35 PM
      Emma Carter confirmed on the phone, she signs tomorrow morning.
    2. Liam BrooksDirector09/24/2026, 2:42 PM
      Approved on my side. Net 30 payment terms, same as the last contract.
  2. Ryan Martin09/24/2026, 3:10 PM

    Comment deleted

    1. Michael HayesAuthor09/24/2026, 3:18 PM
      @Ryan Martin I fixed the amount in the CRM, it is $12,480.00.
  3. Jessica Dunn09/24/2026, 3:48 PM
    The proforma invoice is ready: PF-2026-044 It is also in the client folder: https://crm.summitcrm.com/clients/c01/documents
    PF-2026-044.pdfPDF · 184 KB
CtrlEnterControl Enterto send

Send a few comments: every third request fails, so you can see Retry. Type @ to mention, press Up in the empty box to edit your last comment.⌘/Ctrl+Enter sends · Delete asks first