The line under a post that leads to its thread: who replied, "N replies" (a button that opens the thread) and when the last reply came. Before anyone has replied it is a quiet "Reply" button with a reply arrow, to start the thread, which ends a post's name line and shows on hover and focus where a mouse points.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/thread-summary#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/thread-summaryIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/thread-summary#v1.3.10 ourostack/ouro-ui/thread-summary-tests#v1.3.10import { ThreadSummary } from "@/components/ouro/message/thread-summary";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<ThreadSummary count={thread.replies.length} repliers={thread.repliers} lastReply={formatTime(thread.lastReplyAt)} onOpen={() => openThread(thread.id)} />Usage
Pass it to Message's thread, under a variant="post" message, or a chat message in a channel without threads whose message has replies.
Use instead when
- Use
Buttoninstead for an action that is not about a thread, andAvatarGroupalone to show who is in something.
Props
Type: ThreadSummaryProps.
className- Type:
string - Layout only: margins (
mt-1). Appearance classes warn in development. count(required)- Type:
number - How many replies the thread has. With none, the summary is a single small "Reply" button that opens the thread to write the first one. As a post's
thread, that button sits at the end of the post's line of name and time, so it adds no row. Where a mouse or trackpad points, it shows only while the pointer is over the post or focus is in it; on a touch screen it always shows, in the same place. label- Type:
string - What screen readers hear for the button, when its words alone ("2 replies") would repeat from post to post: "2 replies to Riley's post Settings mockups". Start it with the words the button shows, so speech input that says them still finds it (WCAG's label in name).
lastReply- Type:
string - When the last reply came, already formatted for the reader ("7:44 AM").
lastReplyDateTime- Type:
string - The same moment for machines, as an ISO date: the
<time>element'sdateTime. lastReplyLabel- Type:
((time: string) => string) - Default:
(time) => `Last reply ${time}` - The words around the last reply's time, for other languages.
onOpen(required)- Type:
(event: MouseEvent<HTMLElement>) => void - Opens the thread, with the click event, so the app can check
event.isTrusted. ref- Type:
Ref<HTMLDivElement> - Reaches the rendered element.
repliers- Type:
readonly ThreadReplier[] - Who replied, in the order to show them. The first three show as small overlapping faces: the picture, or one initial on the person's colour; the count beside them says how many replied in all.
repliersLabel- Type:
string - Default:
"Replied" - What screen readers hear for the repliers' avatars, for other languages.
repliesLabel- Type:
((count: number) => string) - Default:
(count) => count === 1 ? "1 reply" : `${count} replies` - The button's words for a count, for other languages.
replyLabel- Type:
string - Default:
"Reply" - The button's words before anyone has replied, for other languages.
Native attributes
Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
onClick: Not a prop. The thread opens withonOpen:<ThreadSummary count={2} onOpen={openThread}>.replyCount: Not a prop. The number of replies iscount:<ThreadSummary count={replies.length}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: ThreadSummary merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ThreadReplier
Someone who replied in a thread: their name, and their picture if there is one.
interface ThreadReplier {
/** Their name, for the avatar's initials and for screen readers. */
name: string;
/** Their picture's URL. Without it the avatar shows initials. */
src?: string | undefined;
}Claim sources
- layout-classes: test:registry/ouro/components/ouro/message/thread-summary.test.tsx::ThreadSummary > forwards a ref and lets app layout classes win
verify:testExists(registry/ouro/components/ouro/message/thread-summary.test.tsx::ThreadSummary > forwards a ref and lets app layout classes win)
