# Description List

A list of read-only facts about one record, as labels and values.

```tsx
import { DescriptionItem, DescriptionList, StatusBadge } from 'ferry-ui'

export default function DescriptionListHero() {
  return (
    <DescriptionList aria-label="Order details">
      <DescriptionItem label="Order" mono>
        ORD-58213
      </DescriptionItem>
      <DescriptionItem label="Status">
        <StatusBadge tone="success" label="Fulfilled" />
      </DescriptionItem>
      <DescriptionItem label="Customer">Acme</DescriptionItem>
      <DescriptionItem label="Payment">Bank transfer</DescriptionItem>
      <DescriptionItem label="Placed">Mar 4, 2026</DescriptionItem>
      <DescriptionItem label="Shipped">Mar 5, 2026</DescriptionItem>
      <DescriptionItem label="Total">
        <span className="tabular">$1,284.00</span>
      </DescriptionItem>
      <DescriptionItem label="Tracking" mono>
        TRK-0123-4567
      </DescriptionItem>
    </DescriptionList>
  )
}
```

## Usage guidelines

- **Facts about one record.** Use it for the status, the dates and the amounts of one invoice, one order or one member.
- **Many records are a table.** To compare records by column, use [Table](/docs/components/table).
- **Read only.** For settings that the user can edit, use [Form Card](/docs/components/form-card).
- **Not for a headline number.** For a KPI, use [Metric Card](/docs/components/metric-card). For facts with an icon, use [Info Tile](/docs/components/info-tile).

## Anatomy

Import the two parts. The list is a `<dl>` element. The children of an item are its value.

```tsx title="Anatomy"

<DescriptionList>
  <DescriptionItem label="" />
  <DescriptionItem label="" />
</DescriptionList>
```

The `variant` prop of the list sets the look. The items get the variant from the list.

| Variant | Look | Use |
| --- | --- | --- |
| `grid` (default) | A card of cells. | The facts at the top of a detail page. |
| `strip` | One row of cells. | Short values at the bottom of a card. |
| `rows` | Rows with the value on the right. | A side panel or the body of a card. |
| `inline` | Two compact columns with no frame. | A popover or a menu. |

## Examples

### Columns

`columns` sets the number of columns of the `grid` and `strip` variants, from 1 to 4. The default is 4. The `grid` variant uses fewer columns on a small screen.

```tsx
import { DescriptionItem, DescriptionList } from 'ferry-ui'

export default function DescriptionListColumns() {
  return (
    <div className="flex flex-col gap-6">
      <DescriptionList columns={2} aria-label="Project">
        <DescriptionItem label="Owner">Maya Chen</DescriptionItem>
        <DescriptionItem label="Visibility">Private</DescriptionItem>
      </DescriptionList>
      <DescriptionList columns={3} aria-label="Subscription">
        <DescriptionItem label="Plan">Business</DescriptionItem>
        <DescriptionItem label="Seats">18 of 25</DescriptionItem>
        <DescriptionItem label="Renews">Apr 1, 2026</DescriptionItem>
      </DescriptionList>
    </div>
  )
}
```

### Long values

A value stays on one line. `span` makes a cell of the grid wider. `wrap` lets the text continue on more lines. `mono` is for an ID, a URL or a key.

```tsx
import { DescriptionItem, DescriptionList } from 'ferry-ui'

const ENDPOINT = 'https://hooks.example.com/v2/workspaces/acme/integrations/billing-events/receiver'

export default function DescriptionListLongValues() {
  return (
    <DescriptionList aria-label="Webhook details">
      <DescriptionItem label="Endpoint" mono span={2}>
        <span title={ENDPOINT}>{ENDPOINT}</span>
      </DescriptionItem>
      <DescriptionItem label="Events">invoice.paid</DescriptionItem>
      <DescriptionItem label="Last delivery">2 minutes ago</DescriptionItem>
      <DescriptionItem label="Description" span="full" wrap>
        Sends the paid invoices to the data warehouse of the finance team. A delivery that fails starts again for 24
        hours, then the billing channel gets an alert.
      </DescriptionItem>
    </DescriptionList>
  )
}
```

### Strip

The `strip` variant goes at the bottom of a [Card](/docs/components/card). It always shows the number of columns that you set. Keep the values short.

```tsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle, DescriptionItem, DescriptionList } from 'ferry-ui'

export default function DescriptionListStrip() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <div>
          <CardTitle>Design team</CardTitle>
          <CardDescription>The workspace for product design.</CardDescription>
        </div>
      </CardHeader>
      <CardContent className="text-[13px] text-foreground-light">
        The brand assets and the component library are here.
      </CardContent>
      <DescriptionList variant="strip" columns={3} aria-label="Team details">
        <DescriptionItem label="Members">12</DescriptionItem>
        <DescriptionItem label="Projects">7</DescriptionItem>
        <DescriptionItem label="Team ID" mono>
          tm_42
        </DescriptionItem>
      </DescriptionList>
    </Card>
  )
}
```

### Rows

In the `rows` variant, an item can have an `icon`, a `hint` and an `href`. With `href`, the full row is a link. `divided={false}` removes the line above the first row.

```tsx
import { Card, CardHeader, CardTitle, DescriptionItem, DescriptionList, type LinkComponent } from 'ferry-ui'
import { Globe, KeyRound, Tag, Users } from 'lucide-react'

// In an app, the link component of your router opens the page. This one stays on the page.
const DemoLink: LinkComponent = ({ href, onClick, ...props }) => (
  <a
    href={href}
    {...props}
    onClick={(event) => {
      onClick?.(event)
      event.preventDefault()
    }}
  />
)

export default function DescriptionListRows() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Acme workspace</CardTitle>
      </CardHeader>
      <DescriptionList variant="rows" divided={false} aria-label="Workspace summary">
        <DescriptionItem icon={<Users />} label="Members" hint="2 pending" href="#members" linkComponent={DemoLink}>
          18
        </DescriptionItem>
        <DescriptionItem icon={<KeyRound />} label="API keys" href="#api-keys" linkComponent={DemoLink}>
          4
        </DescriptionItem>
        <DescriptionItem icon={<Tag />} label="API version" mono>
          2026-03-01
        </DescriptionItem>
        <DescriptionItem icon={<Globe />} label="Region" mono>
          eu-west
        </DescriptionItem>
      </DescriptionList>
    </Card>
  )
}
```

### Inline

The `inline` variant has no frame. Use it in a [Popover](/docs/components/popover) or in a menu.

```tsx
import {
  Button,
  DescriptionItem,
  DescriptionList,
  Popover,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from 'ferry-ui'
import { Building2 } from 'lucide-react'

export default function DescriptionListInline() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button icon={<Building2 />}>Acme workspace</Button>
      </PopoverTrigger>
      <PopoverContent align="start" className="flex w-72 flex-col gap-3">
        <PopoverHeader>
          <PopoverTitle>Acme workspace</PopoverTitle>
        </PopoverHeader>
        <DescriptionList variant="inline">
          <DescriptionItem label="Plan">Business</DescriptionItem>
          <DescriptionItem label="Currency" mono>
            EUR
          </DescriptionItem>
          <DescriptionItem label="Domain" mono>
            acme.example.com
          </DescriptionItem>
          <DescriptionItem label="Owner">Maya Chen</DescriptionItem>
        </DescriptionList>
      </PopoverContent>
    </Popover>
  )
}
```

### Loading

Set `loading` on an item while its value loads. The item shows a skeleton and sets `aria-busy`.

```tsx
import * as React from 'react'
import { DescriptionItem, DescriptionList, Label, Switch } from 'ferry-ui'

export default function DescriptionListLoading() {
  // In an app, `loading` comes from the request that loads the record.
  const [loading, setLoading] = React.useState(true)

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2">
        <Switch id="customer-loading" checked={loading} onCheckedChange={setLoading} />
        <Label htmlFor="customer-loading">Loading</Label>
      </div>
      <DescriptionList aria-label="Customer details">
        <DescriptionItem label="Customer" loading={loading}>
          Acme
        </DescriptionItem>
        <DescriptionItem label="Plan" loading={loading}>
          Business
        </DescriptionItem>
        <DescriptionItem label="Owner" loading={loading}>
          Maya Chen
        </DescriptionItem>
        <DescriptionItem label="Customer since" loading={loading}>
          Mar 4, 2024
        </DescriptionItem>
      </DescriptionList>
    </div>
  )
}
```

### Empty values

An item with no value shows a dash. The number `0` is a value.

```tsx
import { DescriptionItem, DescriptionList } from 'ferry-ui'

export default function DescriptionListEmptyValues() {
  return (
    <DescriptionList aria-label="Task details">
      <DescriptionItem label="Assignee" />
      <DescriptionItem label="Due date">{null}</DescriptionItem>
      <DescriptionItem label="Notes">{''}</DescriptionItem>
      <DescriptionItem label="Open tasks">{0}</DescriptionItem>
    </DescriptionList>
  )
}
```

## Accessibility

- The list cuts a long value. Put the full text in a `title` attribute.
- In a row with `href`, the label is the text of the link. The value is its description.
- Do not put a button or a second link in a row with `href`. The link covers the full row.

## API reference

`DescriptionList` also accepts each attribute of the `<dl>` element. `DescriptionItem` accepts each attribute of the `<div>` element.

### DescriptionList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"grid" \| "inline" \| "strip" \| "rows"` | `grid` | Look of the list (see `DescriptionListVariant`). Defaults to `grid`. |
| `columns` | `1 \| 4 \| 3 \| 2` | `4` | Columns of the `grid` and `strip` variants (ignored by `rows` and `inline`). Defaults to 4. `grid` is responsive: one column on phones, then 2 → `sm:2`, 3 → `sm:3`, 4 → `sm:2 lg:4`. `strip` always uses exactly this many columns, so keep it to short values. |
| `divided` | `boolean` | `true` | `rows` only. `true` (default) also draws a hairline above the first row, separating the list from a header above it. Set `false` when the list is the first thing in its container. |
| `children` | `ReactNode` |  | `DescriptionItem` elements. |

### DescriptionItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | Short caption (1–3 words: "Status", "Issued", "Billing email"). Rendered in the `<dt>`. |
| `children` | `ReactNode` |  | The value, rendered in the `<dd>` and truncated to one line (unless `wrap`): put long text in `<span title={…}>` so the full value stays reachable. `undefined`, `null`, `false` and `''` render a muted "—" (0 is shown as is). |
| `mono` | `boolean` | `false` | Sets the value in the monospace font at 13px: IDs, hashes, versions, URLs, keys. |
| `wrap` | `boolean` | `false` | Lets a long value wrap onto several lines instead of truncating it to one: a description, a postal address, a full URL or command. Meant for the `grid` variant, paired with `span`; the other variants are built for short values. Defaults to `false`. |
| `icon` | `ReactNode` |  | `rows` only. Decorative 16px icon before the label (any `svg` is sized automatically). |
| `hint` | `ReactNode` |  | `rows` only. Muted secondary text before the value ("3 active", "of 10"). Hidden on phones. |
| `span` | `1 \| 2 \| "full"` | `1` | `grid` only. Cell width: `2` spans two columns from `sm` up (for long values such as a URL or an address), `'full'` spans the whole row. Defaults to 1. |
| `href` | `string` |  | `rows` only. Turns the whole row into a link to this URL (hover and focus highlight). The label is the link text and the value its accessible description. Keep other interactive elements (buttons, copy actions) out of a linked row: the link covers the whole row. |
| `linkComponent` | `LinkComponent` |  | Router link used when `href` is set. Defaults to the one from `LinkProvider` (a plain `<a>`). |
| `loading` | `boolean` | `false` | Shows a skeleton while the value loads and sets `aria-busy`. `rows` and `inline` keep the label; `grid` and `strip` skeleton the label too (the label stays available to screen readers). |
| `valueClassName` | `string` |  | Extra classes for the `<dd>` (e.g. `text-warning`, `flex items-center gap-1.5`). |
