# DescriptionList

One record’s fields, as a real <dl> rather than a grid of divs.

- Group: Surfaces
- Import: `import { DescriptionList } from '@misoto22/design'`
- Page: https://ui.misoto22.com/components/description-list/
- Related: table, card, timestamp

## When to reach for it

One record seen from the front — a detail page, a summary panel. Several records seen from above is a Table.

## Anatomy

- **List** (required) — The <dl>. It is the element that carries the pairing: a grid of divs looks identical and tells a screen reader there are two columns of unrelated text.
- **Pair** (required) — A <div> around each dt/dd, which the HTML specification allows inside a <dl> precisely so a pair can be laid out as a unit. It is what the hairline is drawn on, so the rule crosses the whole row rather than stopping in the column gap.
- **Term** (required) — item.term, rendered as <dt> at --ink-3-aa. In the row layout it holds a 12rem column at sm and above and stacks below it, because a 12rem label column on a phone leaves the value about eight characters wide.
- **Description** (required) — item.description, rendered as <dd> at --ink-2 with its browser margin reset. It takes a node, not a string, so a value can be a Badge, a link or a Timestamp.
- **Hairline** — divided, on by default: a --rule under every pair but the last. Turn it off inside a Card, which already has an edge of its own.

## Best practices

### Do

- Let it render nothing when items is empty. It returns null rather than an empty bordered box, so the page above is free to show an EmptyState instead of a hairline around no content.
- Put an element in description when the value is a state or a link — it is a <dd>, so a Badge, an anchor or a Timestamp belongs there and a string of text does not have to be faked into one.
- Pass id on each item when rows are added, removed or reordered. The index is the key without one, which is right for the fixed field list a record page renders and wrong for a list that changes shape.

### Don’t

- Do not reach for it to show several records. Every dt would repeat down the page and a reader comparing two records has to hold both in their head — that is what a Table’s column headings exist to avoid.
- Do not use layout="row" inside a narrow sidebar. It only collapses on the sm breakpoint, which is the viewport, not the container — a 12rem label column inside a 20rem panel leaves nothing for the value. Use layout="stacked" there.

## Accessibility

- A real <dl>, <dt> and <dd>, which is what tells a screen reader that a label names the value beside it.
- Each pair is grouped in a <div>, which the specification permits inside a <dl> and which assistive technology reads through.
- An empty list renders null rather than an empty <dl>, so nothing announces a list with no items in it.

## DescriptionList

A record's fields: label, value, label, value. The single most repeated shape in any detail page, and the one most often hand-built out of a `<div>` grid — which loses the only thing the markup was carrying. A `<dl>` tells a screen reader that "Owner" names the thing beside it; a grid of divs tells it there are two columns of unrelated text, and the reader has to infer the pairing from reading order alone. Reach for `Table` instead when there are several records. This is one record seen from the front; a table is many records seen from above. Each pair is wrapped in a `<div>` — which the HTML specification allows inside a `<dl>` precisely so a pair can be laid out as a unit — so the hairline runs the full width of the row rather than stopping in the column gap. An empty `items` renders `null` rather than an empty bordered box. A record with no fields is a state the page above should be handling with an `EmptyState`, and a hairline around nothing looks like a component that failed to load.

### Props

- `items` (required) — `DescriptionListItem[]`. The pairs, in the order they should read. An empty array renders nothing.
- `layout` — `DescriptionListLayout` default `'row'`. How the value sits relative to its label. See DescriptionListLayout.
- `divided` — `boolean` default `true`. Draws a hairline under every pair but the last. Off for a list inside a card that already has an edge, on for a list that is the page's structure.

Also accepts: `HTMLAttributes<HTMLDListElement>`.

## Example — record fields

```tsx
import { Badge, DescriptionList, Timestamp } from '@misoto22/design'

<DescriptionList
  items={[
    { term: 'Owner', description: 'Henry Chen' },
    { term: 'Region', description: 'ap-southeast-2' },
    { term: 'Status', description: <Badge tone="success">Deployed</Badge> },
    {
      term: 'Last deploy',
      description: <Timestamp value="2026-01-14T09:30:00.000Z" />,
    },
  ]}
/>
```

## Example — row and stacked

```tsx
import { DescriptionList } from '@misoto22/design'

<div className="grid w-full gap-8 sm:grid-cols-2">
  <DescriptionList items={FIELDS} />
  <DescriptionList layout="stacked" items={FIELDS} />
</div>
```

## Example — a metadata rail

```tsx
import { Badge, DescriptionList, Heading, Text, Timestamp } from '@misoto22/design'

<div className="grid w-full gap-6 sm:grid-cols-[minmax(0,1fr)_14rem]">
  <div className="flex flex-col gap-3">
    <Heading level={3}>ui.misoto22.com</Heading>
    <Text size="sm">
      The documentation site for the design package. Statically exported, so every
      page is HTML on a CDN and the only JavaScript is the parts a reader touches.
    </Text>
  </div>
  <aside className="rounded-(--radius) border border-(--rule-2) p-4">
    <DescriptionList
      layout="stacked"
      divided={false}
      items={[
        { term: 'Owner', description: 'Henry Chen' },
        { term: 'Status', description: <Badge tone="success">Deployed</Badge> },
        { term: 'Region', description: 'ap-southeast-2' },
        {
          term: 'Last deploy',
          description: <Timestamp value="2026-08-28T22:14:00.000Z" />,
        },
      ]}
    />
  </aside>
</div>
```
