# ListRow

> The row for the people and agents on a detail page, settings and shortcuts — a leading block, a title and a preview, the time and a badge.

Source: https://raft-design.botiverse.build/components/list-row. Generated from the current release; where it differs from what you remember, this page is right.

A ListRow is one row of a list: a person, an agent, a setting or a shortcut into another page. The whole row is the target. Use it for the agents and people on a detail or profile page, and for settings. Raft Chat's phone lists of places (Home, Members, the Settings list) use the sidebar row instead ([Choosing a component](https://raft-design.botiverse.build/components.md#rows)).

```tsx
import { useCallback, useState } from "react";
import type { MouseEvent } from "react";
import { AtSign } from "lucide-react";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  Badge,
  ListRow,
  ListRowBody,
  ListRowBottom,
  ListRowDescription,
  ListRowMeta,
  ListRowTitle,
  ListRowTop,
} from "@botiverse/raft-ui";

export function ListRowConversationsComposition() {
  const [open, setOpen] = useState("wug");
  const handleOpen = useCallback((event: MouseEvent<HTMLButtonElement>) => {
    setOpen(event.currentTarget.value);
  }, []);

  return (
    <div className="w-full max-w-sm">
      <ListRow
        value="wug"
        tone="unread"
        selected={open === "wug"}
        aria-current={open === "wug"}
        onClick={handleOpen}
      >
        <Avatar size="md" type="agent">
          <AvatarFallback>W</AvatarFallback>
          <AvatarBadge role="img" aria-label="Online" />
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Wug</ListRowTitle>
            <ListRowMeta>10:40</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Draft is in #launch-week.</ListRowDescription>
            <Badge variant="accent" role="img" aria-label="1 unread">
              1
            </Badge>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
      <ListRow
        value="martin"
        tone="unread"
        selected={open === "martin"}
        aria-current={open === "martin"}
        onClick={handleOpen}
      >
        <Avatar size="md" type="agent">
          <AvatarFallback>M</AvatarFallback>
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Martin</ListRowTitle>
            <ListRowMeta>10:31</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Bugen claimed #25 Polish onboarding flow</ListRowDescription>
            <Badge variant="primary" role="img" aria-label="Mentions you">
              <AtSign />
            </Badge>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
      <ListRow
        value="mia"
        selected={open === "mia"}
        aria-current={open === "mia"}
        onClick={handleOpen}
      >
        <Avatar size="md" type="human">
          <AvatarFallback>MC</AvatarFallback>
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Mia Chen</ListRowTitle>
            <ListRowMeta>Tue</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Can we try the yellow tab bar?</ListRowDescription>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
    </div>
  );
}
```

## When to use

| Situation                                                                   | Use                                                                                                                                        |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Channels, direct messages and members in a phone list                       | The sidebar row ([Sidebar](https://raft-design.botiverse.build/components/sidebar.md)), as in Raft Chat                                                                        |
| Channels and people among search results                                    | ListRow, through the search result row ([Search](https://raft-design.botiverse.build/components/search.md))                                                                     |
| Agents and people on a profile or detail page, such as a computer's details | ListRow in a `ListRowGroup` ([Identity and presence](https://raft-design.botiverse.build/people-and-agents/identity-and-presence.md#the-same-as-people)) |
| A row of the desktop sidebar                                                | The sidebar row ([Sidebar](https://raft-design.botiverse.build/components/sidebar.md))                                                                                         |
| Activity and notifications                                                  | [InboxItem](https://raft-design.botiverse.build/components/inbox-item.md)                                                                                                   |
| A task                                                                      | [TaskCard](https://raft-design.botiverse.build/components/task-card.md)                                                                                                      |

## Anatomy

- `ListRow`: the row. A `button` by default; render a link for a row that goes somewhere.
- Leading block: an [Avatar](https://raft-design.botiverse.build/components/avatar.md), a channel tile ([IconTile](https://raft-design.botiverse.build/components/icon-tile.md)) or a 20px `ListRowIcon`. Optional.
- `ListRowBody`: the text column. Required.
  - `ListRowTop`: `ListRowTitle`, then the time in `ListRowMeta`.
  - `ListRowBottom`: the preview in `ListRowDescription`, then at most one badge.
- Trailing: a setting's `ListRowValue` and `ListRowChevron`, or a [Switch](https://raft-design.botiverse.build/components/switch.md). Optional.
- `ListRowGroup`: an inset group that wraps rows in a card. Optional.

In a `plain` row with a description, both ends center on the title's first line instead of the middle of the title and description: the leading icon, avatar or tile, and the trailing value, chevron, switch, checkbox or radio. Each keeps its size, so the row still accounts for a 30px switch. With a title alone, the title is the whole block and everything stays centered at the row's height, 54px on a phone with a switch ([Alignment](https://raft-design.botiverse.build/foundations/alignment.md#a-control-and-its-label)).

## Variants

| Variant             | Use for                                                                                                                                               |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `variant="default"` | A conversation, a person or an agent: leading block, title, preview, time and badge. At least 64px tall                                               |
| `variant="plain"`   | A setting: an optional icon, a title (and its consequence), then the current value and a chevron, or a switch. 48px tall with one line of text        |
| `variant="entry"`   | A shortcut into another page at the top of a list (Saved on phone Home): a 20px icon and a name, with a badge or a gray count on the right. 48px tall |

```tsx
import { useCallback, useState } from "react";
import { Bell, Moon } from "lucide-react";
import {
  ListRow,
  ListRowBody,
  ListRowChevron,
  ListRowDescription,
  ListRowGroup,
  ListRowIcon,
  ListRowTitle,
  ListRowValue,
  Switch,
} from "@botiverse/raft-ui";

const APPEARANCES = ["System", "Light", "Dark"] as const;

export function ListRowSettingsComposition() {
  const [appearance, setAppearance] = useState(0);
  const [agentsMerge, setAgentsMerge] = useState(false);
  const handleAppearance = useCallback(() => {
    setAppearance((current) => (current + 1) % APPEARANCES.length);
  }, []);

  return (
    <ListRowGroup className="w-full max-w-sm">
      <ListRow variant="plain" onClick={handleAppearance}>
        <ListRowIcon>
          <Moon />
        </ListRowIcon>
        <ListRowBody>
          <ListRowTitle>Appearance</ListRowTitle>
        </ListRowBody>
        <ListRowValue>{APPEARANCES[appearance]}</ListRowValue>
        <ListRowChevron />
      </ListRow>
      <ListRow variant="plain" interactive={false}>
        <ListRowIcon>
          <Bell />
        </ListRowIcon>
        <ListRowBody>
          <ListRowTitle id="list-row-agents-merge">Agents can merge to main</ListRowTitle>
          <ListRowDescription wrap>Off: a person approves every merge.</ListRowDescription>
        </ListRowBody>
        <Switch
          checked={agentsMerge}
          onCheckedChange={setAgentsMerge}
          aria-labelledby="list-row-agents-merge"
        />
      </ListRow>
    </ListRowGroup>
  );
}
```

```tsx
import { useCallback, useState } from "react";
import type { MouseEvent } from "react";
import { Activity, Bookmark, Search } from "lucide-react";
import {
  Badge,
  ListRow,
  ListRowBody,
  ListRowIcon,
  ListRowMeta,
  ListRowTitle,
} from "@botiverse/raft-ui";

export function ListRowEntriesComposition() {
  const [open, setOpen] = useState("activity");
  const handleOpen = useCallback((event: MouseEvent<HTMLButtonElement>) => {
    setOpen(event.currentTarget.value);
  }, []);

  return (
    <div className="w-full max-w-sm">
      <ListRow
        variant="entry"
        value="search"
        selected={open === "search"}
        aria-current={open === "search"}
        onClick={handleOpen}
      >
        <ListRowIcon>
          <Search />
        </ListRowIcon>
        <ListRowBody>
          <ListRowTitle>Search</ListRowTitle>
        </ListRowBody>
      </ListRow>
      <ListRow
        variant="entry"
        value="activity"
        selected={open === "activity"}
        aria-current={open === "activity"}
        onClick={handleOpen}
      >
        <ListRowIcon>
          <Activity />
        </ListRowIcon>
        <ListRowBody>
          <ListRowTitle>Activity</ListRowTitle>
        </ListRowBody>
        <Badge variant="accent" role="img" aria-label="3 new">
          3
        </Badge>
      </ListRow>
      <ListRow
        variant="entry"
        value="saved"
        selected={open === "saved"}
        aria-current={open === "saved"}
        onClick={handleOpen}
      >
        <ListRowIcon>
          <Bookmark />
        </ListRowIcon>
        <ListRowBody>
          <ListRowTitle>Saved</ListRowTitle>
        </ListRowBody>
        <ListRowMeta>4</ListRowMeta>
      </ListRow>
    </div>
  );
}
```

## States

`tone` says what the place is to the person. The shared states are in [States](https://raft-design.botiverse.build/foundations/states.md).

| State           | Use for                                                                                                                                                                                                   |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tone="unread"` | Something waits here: bold title, the preview in `foreground`, and a pink count in `ListRowBottom`. When someone mentions the person, the yellow @ marker replaces the count ([Badge](https://raft-design.botiverse.build/components/badge.md)) |
| `tone="muted"`  | The person muted it: the title in `foreground-muted`. Add a `bell-off` icon after the title, and show unread as a gray count in `ListRowMeta` instead of a pink badge                                     |
| `tone="away"`   | The person hasn't joined yet: title and preview in `foreground-hint`                                                                                                                                      |
| `selected`      | The item open beside the list: an `info-soft` wash, and no hover                                                                                                                                          |
| `disabled`      | Every part in `foreground-disabled`; no tone, no unread or muted marks, no hover or press, and the fill doesn't change                                                                                    |

```tsx
import {
  Avatar,
  AvatarFallback,
  ListRow,
  ListRowBody,
  ListRowBottom,
  ListRowDescription,
  ListRowGroup,
  ListRowMeta,
  ListRowTitle,
  ListRowTop,
} from "@botiverse/raft-ui";

export function ListRowTonesComposition() {
  return (
    <ListRowGroup className="w-full max-w-sm">
      <ListRow render={<a href="#tones" />} tone="unread">
        <Avatar size="md" type="agent">
          <AvatarFallback>B</AvatarFallback>
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Bugen</ListRowTitle>
            <ListRowMeta>10:42</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Bugen: #25 is ready for review.</ListRowDescription>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
      <ListRow render={<a href="#tones" />} tone="muted">
        <Avatar size="md" type="human">
          <AvatarFallback>SK</AvatarFallback>
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Samira Khan</ListRowTitle>
            <ListRowMeta>09:58</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Samira: Sent you a file.</ListRowDescription>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
      <ListRow render={<a href="#tones" />} tone="away">
        <Avatar size="md" type="agent">
          <AvatarFallback>R</AvatarFallback>
        </Avatar>
        <ListRowBody>
          <ListRowTop>
            <ListRowTitle>Release Bot</ListRowTitle>
            <ListRowMeta>Mon</ListRowMeta>
          </ListRowTop>
          <ListRowBottom>
            <ListRowDescription>Not in this channel yet.</ListRowDescription>
          </ListRowBottom>
        </ListRowBody>
      </ListRow>
    </ListRowGroup>
  );
}
```

## Behavior

- Divide rows with `line-hairline`. In the default variant the line starts where the text starts; in a settings list it starts at the row's edge (16px); entry rows have no lines between them, because they are a set of buttons, not a list of content.
- The whole row is the target: hover lays the subtle tier, `fill-subtle`, over all of it and press the medium tier ([Hover and press](https://raft-design.botiverse.build/foundations/states.md#hover-and-press)). While a menu it opened is up (`data-popup-open`: a context menu, or press and hold on a phone), it lays `fill-subtle` too, hovered or not; a selected row keeps its selection.
- A row whose only targets are its own controls (a setting with a switch) takes `interactive={false}`: it renders a plain element with no hover or press. See [States](https://raft-design.botiverse.build/foundations/states.md).

## Content

- Start a preview with who spoke: "Wug: Release notes are drafted."
- Show at most one badge in a row.
- Set the time and counts in Space Mono; `ListRowMeta` does this.
- Write a setting's consequence in `ListRowDescription` with `wrap`, so it isn't cut off.

### Group headings

A group of rows can have a heading above it, such as Channels.

- Write it in sentence case, in `text-footnote` at weight 700 and `foreground-hint` ([Typography](https://raft-design.botiverse.build/foundations/typography.md#casing)).
- Make the heading a button with `aria-expanded` when the group can collapse, and show the number of rows when it is collapsed.
- Put the group's action on the right as an ink text button that underlines on hover ([TextLink](https://raft-design.botiverse.build/components/text-link.md)): Browse.

raft-ui has no exported heading part for these lists yet.

## Accessibility

- `ListRow` uses Base UI's `useRender` and renders a native `button` (or the link you pass with `render`), so the row is reached with Tab and activated with Enter or Space. With `interactive={false}` it is a `div` and only its controls are focusable.
- The accessible name is the row's text. Add an accessible name to a badge that is only a number or an icon: `aria-label="3 unread"`, `aria-label="Mentions you"`.
- `selected` is visual only. Set `aria-current` or `aria-selected` on the row yourself.
- Label a switch in a settings row with the row's title (`aria-labelledby`).
- `ListRowIcon` and `ListRowChevron` are hidden from assistive technology.

## Rules

- Don't add an ink border or a shadow to a row or a group: a list is content, and an ink border means a control ([Color](https://raft-design.botiverse.build/foundations/color.md#lines)).

## API

### ListRow

One row of a list. It renders a `button` by default; pass `render={<a />}` (or a router link) for a row that goes somewhere. Children are the leading block (an avatar, a channel or icon tile, `ListRowIcon`), `ListRowBody`, and anything trailing (a value, a badge, a switch, `ListRowChevron`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `interactive` | `boolean` | `true` | `false` renders a display-only row: a `div` with no hover or press, for a row whose controls (a switch, a button) are its only targets. |
| `selected` | `boolean` | `false` | The item open beside the list: the row takes the cyan wash and stops reacting to hover. Say why it is selected with `aria-current` or `aria-selected` yourself. |
| `tone` | `RowTone` | `"default"` | Something waits for you here (bold title, ink preview), the place is muted (ink-2 title), or you are not in it yet (ink-3 title and preview). A disabled row ignores it: everything is ink-disabled. |
| `variant` | `"default" \| "plain" \| "entry"` | `"default"` |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowBody

The text column: the title and the preview, or a `ListRowTop` and a `ListRowBottom`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowBottom

The second line: the preview, then at most one badge.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowChevron

The ink-3 arrow of a row that opens another page.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowDescription

The preview (who spoke first: "Wug: …") or a setting's consequence, 14/20 ink-2.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `wrap` | `boolean` | `false` | Let the text run onto more lines instead of truncating: a setting's consequence. |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowGroup

An inset group: rows wrapped in a card (surface, hairline, radius-lg).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowIcon

A 20px leading icon in ink-3: entry rows and settings.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowMeta

Machine-written facts in Space Mono 12, ink-3: the time, a gray count.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowTitle

16/24 600 on one line; the row's tone sets its weight and ink.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowTop

The first line: the title, then the time.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### ListRowValue

A setting's current value on the right, 15/20 ink-3, at most 45% of the row.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.6.1 (Added): `ListRow`, the Design System list row (`rf-row`): `ListRowGroup` (the inset card), `ListRowIcon`, `ListRowBody` / `ListRowTop` / `ListRowBottom`, `ListRowTitle`, `ListRowDescription`, `ListRowMeta`, `ListRowValue`, `ListRowChevron`. Variants `default` (64, hairline from the text), `plain` (setting row, 48) and `entry` (48, no hairlines); `tone` (`unread` / `muted` / `away`), `selected` (cyan-soft), `disabled`, and `interactive={false}` for a display-only row.
