# ListItem

**When to use:** One row of a list that stands for a record: a customer, an invoice, a delivery note. It shows a title, a second line and a status, can hold actions of its own, and can open the record from the whole row. For hundreds of rows use [VirtualList](VirtualList.md); for actions hanging off a trigger, [Menu](Menu.md).

**Keywords:** row, list row, cell, entry, record, item, drill-down, detail link, settings row

**Import:** `import { ListItem } from '@ahrowe/ui'`
**Types:** `import type { ListItemProps } from '@ahrowe/ui'`

```tsx
import { ActionIcon, Chip, ListItem, ThemeColor } from '@ahrowe/ui';
import { faFileInvoice, faPen } from '@fortawesome/free-solid-svg-icons';

// Shows a record
<ListItem title="Created" endContent="12 Oct 2026" />

// Opens it: the whole row is the link, with a chevron
<ListItem
  icon={faFileInvoice}
  title="Invoice 2026-0042"
  subtitle="Müller GmbH, due 1 Nov"
  endContent={<Chip text="Open" color={ThemeColor.Warn} />}
  href="/invoices/42"
/>

// Opens it and has an action: the edit button stays its own
<ListItem
  title="Müller GmbH"
  subtitle="3 projects"
  href="/clients/7"
  endContent={<ActionIcon icon={faPen} aria-label="Edit" onClick={editClient} />}
/>

// Opens it from a button instead of a link: no chevron, which promises a page
<ListItem title="Add a contact" onClick={openContactModal} />

// The one that is open, beside its detail view
<ListItem title="Müller GmbH" href="/clients/7" selected={clientId === 7} />
```

## Opening a record

With `href` the title is a link, and with `onClick` a button. Either one stretches over the whole
row, so a click anywhere on it opens the record, and with a link a middle click and a long press
too. Controls in the row stay their own: a button, a link, an input or anything with a `tabindex`
sits above the stretched area. Text around them still opens the row, so a click on a status `Chip`
opens the record.

A plain click on a page of the app goes through `ConfigProvider`'s `navigate` when it is set, see
[ConfigProvider.md](ConfigProvider.md). A screen reader hears the title as the link's name and the
subtitle, and a text `endContent`, as its description, rather than the whole row run together.

Because the link covers the row, its text cannot be selected with the mouse. A row that is only
read, with neither `href` nor `onClick`, selects as usual.

The rows sit in whatever holds them, a `Card` or a `<ul>` of your own. ListItem draws no dividers
and no list semantics of its own: it is a `<div>`, so in a `<ul>` give each one an `<li>`.

A row takes its container's width and cuts its text off there, so stack rows in a block or a flex
column. In a grid, give the column `minmax(0, 1fr)`: a plain `display: grid` column is `auto`, which
grows to fit the whole title plus the `endContent`, and on a phone runs out of the card.

**Dividers.** Between groups of rows, a [Divider](Divider.md): it has no margin, so it adds the line
and nothing else. Between every row, a border from the container instead, because a screen reader
announces each `Divider` as a separator. The same rule works on `<div>`s and on `<li>`s:

```css
.clients > * + * {
  border-top: 1px solid var(--border-color);
}
```

Without a stylesheet, the same border inline on every row but the first:
`style={index > 0 ? { borderTop: '1px solid var(--border-color)' } : undefined}`.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `title` | `ReactNode` | The main line, cut off with an ellipsis where it does not fit (required) |
| `subtitle` | `ReactNode` | A smaller line under it, cut off with an ellipsis like the title. To let a description wrap instead: `styles={{ subtitle: { whiteSpace: 'normal' } }}` |
| `icon` | `IconDefinition \| ReactElement` | Before the title: a FontAwesome icon or any element, such as an `Avatar` |
| `endContent` | `ReactNode` | The trailing edge: a status, a date, an amount, and controls of its own. It sizes to its content, so a control that fills its container (`Dropdown`, `Input`) needs a `width` of its own here |
| `href` | `string` | Opens the record from the whole row, as a link. `target`, `rel` and `download` apply to it |
| `onClick` | `MouseEventHandler<HTMLElement>` | Opens the record from the whole row, as a button. With `href` too, runs first, and `preventDefault()` keeps the link from being followed |
| `selected` | `boolean` | The row whose record is open: tinted in `--list-item-selected-background`, and `aria-current="page"` on its link |
| `chevron` | `boolean` | The chevron at the end. On by default for a row with an `href`, off for one with only `onClick`, which opens a dialog or runs an action rather than going somewhere |

Every native attribute passes through to the root `<div>`. A long title shrinks before
`endContent` and the chevron do, so they never leave the row. `endContent` itself never shrinks:
a value longer than a date or an amount belongs in the `subtitle`.

**Theming:**

| Variable | Default |
|----------|---------|
| `--list-item-min-height` | `48px`, the smallest comfortable touch target |
| `--list-item-selected-background` | a 12% tint of `--primary-color` |

Set it once in the theme rather than on each row: `'--list-item-min-height': '64px'`. Hover and
press tint the row in its own text colour, so they show on any surface; the keyboard focus ring
goes around the whole row, inside its edge, in `--focus-border-color`.

**Slots:** `root` `icon` `labels` `trigger` (the link or button, which draws the hover and focus ring in its `::after`) `title` `subtitle` `endContent` `chevron`

## Migration from the two-line subtitle

| Before | Now |
|--------|-----|
| `subtitle` wrapped onto a second line, then was cut off | one line with an ellipsis. For a wrapping description: `styles={{ subtitle: { whiteSpace: 'normal' } }}` |
