<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# ConversationList

A list of conversations to choose from: each row shows the avatar, the title, when the latest message arrived and a line of it, and marks unread conversations with a bold title and a dot. The open conversation's row is highlighted, under the pointer too, and marked `aria-current`. The list is a single stop for the Tab key, which lands on the open conversation; Up and Down arrows move between rows, Home and End jump to the first and last, and Enter, Space or a click calls `onSelect`. Rows can group under `sections`, whose headers open and close them. A list of more than 60 conversations renders only the rows near the view, and the Tab stop and the open row, each placed where it would be in the whole list and measured as it renders, so a long list costs about what a screenful does. The arrow keys, Home and End still reach every row, and the open conversation's row scrolls into view. A long list's other rows are not in the page, so code that reads the rows from the page, and the browser's find in page, see only the rows rendered: read `items` instead.

```tsx
import { ConversationList } from "@/components/ouro/conversation-list/conversation-list";
```

## Example

```tsx
<ConversationList label="Conversations" items={conversations} selectedId={openId} onSelect={setOpenId} />
<ConversationList label="Chats" sections={[{ id: "pinned", title: "Pinned" }]} items={[{ id: "ana", title: "Ana Lind", section: "pinned" }]} onSelect={open} />
```

## Use instead when

- Use `ItemGroup` instead for a static list with nothing open, and `Tabs` for a handful of views that switch in place.

## Props

Type: `ConversationListProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` |  | Layout only: width, margins, grid placement (`w-80`). Appearance classes warn in development. |
| `density` | `"comfortable" \| "compact"` | `"comfortable"` | `compact` drops the preview line and uses a smaller avatar. |
| `draftLabel` | `string` | `"Draft:"` | The words before a row's `draft`, for other languages. |
| `empty` | `ReactNode` | `"Nothing here yet"` | What shows when `items` is empty. |
| `items` (required) | `readonly ConversationListItem[]` |  | The conversations, in the order they show. |
| `label` (required) | `string` |  | What the list is, for screen readers ("Conversations"). Required, so the list has a name. |
| `onSectionOpenChange` | `((id: string, open: boolean) => void)` |  | Called with a section's `id` and whether it is now open, when its header is pressed. |
| `onSelect` (required) | `(id: string, event: MouseEvent<HTMLButtonElement>) => void` |  | Called with a row's `id` when it is chosen by a click, a tap, Enter or Space. A handler that takes a second parameter also gets the click, so the app can check `event.isTrusted` or `event.metaKey`; a one-parameter handler or a server action gets the `id` alone. |
| `ref` | `Ref<HTMLDivElement>` |  | Reaches the rendered element. |
| `sections` | `readonly ConversationListSection[]` |  | The headers the rows group under, in order. A row names its header in `section`, and a section without rows is left out. |
| `selectedId` | `string \| null` |  | The `id` of the open conversation. Its row is highlighted and marked `aria-current`. |
| `unreadLabel` | `string` | `"Unread"` | What screen readers hear for an unread row's dot, and a closed section's, 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.

- `conversations`: Not a prop. The rows are `items`: `<ConversationList items={conversations}>`.
- `groups`: Not a prop. Headers are `sections`, and a row names its own in `section`: `<ConversationList sections={sections} items={rows}>`.
- `onValueChange`: Not a prop. A chosen row calls `onSelect`: `<ConversationList onSelect={(id) => open(id)}>`.
- `selected`: Not a prop. The open conversation is `selectedId`: `<ConversationList selectedId={openId}>`.
- `value`: Not a prop. The open conversation is `selectedId`: `<ConversationList selectedId={openId}>`.

## `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: ConversationList 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

### `ConversationListContentTitle`

A conversation whose title is content, such as its name with emoji drawn as pictures (`<EmojiText text={chat.name} catalogue={catalogue} />`), with its name as text beside it.

```ts
interface ConversationListContentTitle {
  /** What the row shows as the conversation's name. */
  title: ReactNode;
  /**
   * The conversation's name as text: what screen readers hear in place of the title, and the
   * avatar's initials come from it.
   */
  name: string;
}
```

### `ConversationListItem`

One conversation in a `ConversationList`: its title as text, or as content with a `name`.

```ts
type ConversationListItem = ConversationListItemFields & (ConversationListTextTitle | ConversationListContentTitle);
```

### `ConversationListItemFields`

What a `ConversationList` row shows of a conversation, whichever way its title comes.

```ts
interface ConversationListItemFields {
  /** Identifies the conversation; `onSelect` receives it. */
  id: string;
  /**
   * A quiet tag right after the title, such as "AI agent" for an agent or "Guest": small and muted, not a badge, and
   * read after the title ("Halden, AI agent"). In a narrow row the title shortens and the tag stays whole.
   */
  nameTag?: string | undefined;
  /**
   * A muted name after the title, on its line, such as the team a channel belongs to: "Design
   * reviews · Launch team". Text, or content such as `<EmojiText>` to draw its emoji as pictures.
   */
  where?: ReactNode;
  /** A second line under the title, such as the group a topic belongs to. */
  subtitle?: string | undefined;
  /**
   * A line between the title and the preview that says what happened and where, as an activity
   * feed shows it: the title names the person, this line "mentioned you in Design reviews", and
   * the preview the message. Text, or content such as `<EmojiText>`. It keeps to `density="compact"`.
   */
  context?: ReactNode;
  /** The latest message, cut to one line. `density="compact"` hides it. */
  preview?: ReactNode;
  /**
   * The first words of a message the person started here and has not sent. They show in place
   * of the preview after `draftLabel` ("Draft:"), in the destructive tone as chat apps mark a
   * draft, so a half-written reply is visible from the list. `density="compact"` hides it with
   * the preview.
   */
  draft?: ReactNode;
  /** When the latest message arrived, already formatted ("9:41", `<RelativeTime>`). */
  time?: ReactNode;
  /** There are messages the person has not read: the title is bold and a dot shows. */
  unread?: boolean | undefined;
  /** A small icon before the title, in the muted color, such as a pin or a muted bell. Give it a name (`role="img"` and `aria-label`) when it means something. */
  icon?: ReactNode;
  /** Small marks after the title, such as a `<Badge>`. */
  badges?: ReactNode;
  /**
   * The row's mark, at the end of its last line under the time: a mention `<Badge>`, "in 10 min" before a meeting.
   * It takes the unread dot's place; screen readers still hear `unreadLabel`. Words show small and muted, like the
   * time. It keeps to the line's height, so the row stays the height of the others.
   */
  mark?: ReactNode;
  /**
   * The avatar: a picture's URL, or an element to show instead, such as an `<Avatar>` with the person's presence or an
   * `<AvatarGroup>`. Without it the avatar shows the title's initials; `false` hides the avatar.
   */
  avatar?: string | ReactElement | false | undefined;
  /** The `id` of the section in `sections` the row shows under. Rows without a section come first, before the headers. */
  section?: string | undefined;
}
```

### `ConversationListSection`

A group of rows in a `ConversationList`, under a header that opens and closes it.

```ts
interface ConversationListSection {
  /** Identifies the section; rows name it in their `section`, and `onSectionOpenChange` receives it. */
  id: string;
  /** The header's words: "Pinned", "Recent". They also name the section's list for screen readers. */
  title: string;
  /** Muted words after the title on the header, such as "your folder". */
  note?: ReactNode;
  /** Whether the section starts open, when the list keeps its state. @default true */
  defaultOpen?: boolean | undefined;
  /** Whether the section is open, when the app keeps the state with `onSectionOpenChange`. */
  open?: boolean | undefined;
}
```

### `ConversationListTextTitle`

A conversation whose title is text.

```ts
interface ConversationListTextTitle {
  /** The conversation's name: a person, a group or a topic. The avatar's initials come from it. */
  title: string;
  /** The name screen readers hear in place of `title`, and the avatar's initials come from. @default the title */
  name?: string | undefined;
}
```

[^layout-classes]: test:registry/ouro/components/ouro/conversation-list/conversation-list.test.tsx::ConversationList > forwards a ref and lets app layout classes win verify:testExists(registry/ouro/components/ouro/conversation-list/conversation-list.test.tsx::ConversationList > forwards a ref and lets app layout classes win)
