Skip to main content

Chat

ThreadSummary

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.

Jonah Vale,

Agenda for Friday's design review

We will look at the new onboarding flow first, then the settings page.

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then 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.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/thread-summary

Its 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.10
import { 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 Button instead for an action that is not about a thread, and AvatarGroup alone 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's dateTime.
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 with onOpen: <ThreadSummary count={2} onOpen={openThread}>.
  • replyCount: Not a prop. The number of replies is count: <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