Chat

MessageList

A conversation: Messages in a scrolling, labeled region that opens at the newest one. While the reader is at the bottom it follows new messages and streaming text; once they scroll up, nothing moves their view, and a "Jump to latest" button appears. Each new message from someone else is read to screen readers once, politely: a streamed reply when it finishes, and a first reply in a list that mounted empty. History is never announced: the messages it mounts with, several messages filling an empty list at once (history fetched after mount), older messages loaded above, and a message whose key changes in place (a temporary id swapped for the server's). Older messages loaded above a scrolled-up reader leave the message they are reading where it was. Scrolling is never announced.

Ada Lovelace
How long should the dough rest?
Ouro
About an hour, somewhere warm.
Ada Lovelace
Can it rest in the fridge?
Ouro
Yes, overnight in the fridge works too.

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.2.13

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/message-list#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/message-list

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/message-list#v1.2.13 ourostack/ouro-ui/message-list-tests#v1.2.13
import { MessageList } from "@/components/ouro/message/message-list";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<MessageList label="Conversation with Ouro" className="h-[60dvh]">
  {messages.map((message) => (
    <Message key={message.id} role={message.role} name={message.author} streaming={message.streaming}>
      {message.text}
    </Message>
  ))}
</MessageList>

Usage

Give it a height (className="h-96", or flex-1 in a column), since it scrolls inside it.

Use instead when

  • Use a plain list instead for messages that do not arrive live, such as an inbox.

Props

Type: MessageListProps.

children (required)
Type: ReactNode
The Messages, oldest first.
className
Type: string
Layout only: the list's height (h-96, flex-1), margins. It scrolls inside that height, so give it one. Appearance classes warn in development.
jumpLabel
Type: string
Default: "Jump to latest"
The button that appears once the reader scrolls up, for other languages.
label (required)
Type: string
What the conversation is, for screen readers: "Conversation with Ouro". Required, so the region has a name.
newMessageLabel
Type: ((author: string, text: string) => string)
Default: (author, text) => ${author}: ${text}
What screen readers hear when a new message arrives, for other languages. It gets the author's name and the message's text. The user's own messages are not read back.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.

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.

  • autoScroll: Not a prop. The list follows new messages while the reader is at the bottom, and stops when they scroll up.
  • stickToBottom: Not a prop. The list follows new messages while the reader is at the bottom, and stops when they scroll up.

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: MessageList 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.

Claim sources