# List

_Box Kite 2.2.4 · a markdown copy of https://www.box-kite.dev/list/_

A list of things to read and pick from: rows of leading media, text and trailing values, one action covering each item — and not a line of JavaScript, so it renders on a server.

**Import**

```jsx
import List from '@box-kite/react/components/list';
```

**Usage**

```jsx
<List>
  {people.map((person) => (
    <List.Item key={person.id} chevron>
      <List.Media variant="avatar">{person.initials}</List.Media>
      <List.Content>
        <List.Row>
          <List.Title lines={1}>
            <List.Action props={{ href: `#${person.id}` }}>{person.name}</List.Action>
          </List.Title>
          <List.Meta>{person.time}</List.Meta>
        </List.Row>
        <List.Description lines={2}>{person.text}</List.Description>
      </List.Content>
    </List.Item>
  ))}
</List>
```

## An item is a row, and its content is rows

A `List.Item` is a flex row of three slots: `List.Media` leading, `List.Content` taking the room, `List.End` trailing. The content is a column, and each line of it is a `List.Row` — its parts side by side, the first taking the room and the last pushed to the end, so a title and its time, or a status and a count, line up down the list. Every part is optional, and an item with none of them is simply a flex row of whatever it holds.

One line, two or three

A title alone is one line; a `List.Description` under it is two; a `List.Overline` above it, or a description clamped at two lines, is three. That is Material's whole taxonomy, and it falls out of which parts are there.

Text stops where you say

`lines={1}` ends a title or a description in an ellipsis, `2` and `3` clamp it. Unset it wraps, which is right for a name whose end matters as much as its start.

## A main row and a secondary one

`secondary` makes a row the quieter line under the main one — smaller and muted — which is the shape of a mail client, an order list or a pull-request queue: what it is and when on top, its state and its numbers below.

```jsx
<List variant="card">
  <List.Item>
    <List.Content>
      <List.Row>
        <List.Title lines={1}>
          <List.Action>Order 4182 · Grace Hopper</List.Action>
        </List.Title>
        <List.Meta>2 min ago</List.Meta>
      </List.Row>
      <List.Row secondary>
        <Span>Paid · 3 items</Span>
        <Span fontWeight={600} color="gray-900" theme={{ dark: { color: 'white' } }}>€42.00</Span>
      </List.Row>
    </List.Content>
  </List.Item>
</List>
```

## A row that wraps

A row keeps to one line and squeezes its parts, which is what a title and a time want. `wrap` lets the parts go onto a new line instead and starts them all at the start — a set of tags, a list of labels — and on `List.Item` it lets the slots themselves wrap, so a trailing control drops under the text on a narrow screen rather than crushing it.

```jsx
<List>
  <List.Item wrap>
    <List.Content minWidth={48}>
      <List.Title>Design review</List.Title>
      <List.Row wrap>
        {['design', 'research', 'mobile', 'accessibility', 'typography', 'q4', 'roadmap'].map((tag) => (
          <Span key={tag} px={2} borderRadius={999} fontSize={12} bgColor="gray-100" theme={{ dark: { bgColor: 'gray-800' } }}>
            {tag}
          </Span>
        ))}
      </List.Row>
    </List.Content>
    <List.Actions>
      <Button variant="secondary">Join</Button>
    </List.Actions>
  </List.Item>
</List>
```

## Leading and trailing

`List.Media` takes three shapes as variants — `icon` (a tinted square), `avatar` (a circle that clips) and `image` (a larger thumbnail) — or none, for a bare icon. `List.End` stacks a value and its label and aligns them to the end, in tabular figures, so the numbers in a column of items line up. A `chevron` on the item says that pressing it goes somewhere with more.

```jsx
<List variant="card">
  <List.Item chevron>
    <List.Media variant="icon"><Icon size={5}><Wifi /></Icon></List.Media>
    <List.Content>
      <List.Title><List.Action>Wi-Fi</List.Action></List.Title>
    </List.Content>
    <List.End><List.Meta>Home 5G</List.Meta></List.End>
  </List.Item>
  <List.Item>
    <List.Media variant="image" bgGradient={{ linear: 'br', colors: ['sky-400', 'indigo-500'] }} />
    <List.Content>
      <List.Overline>Server · eu-west</List.Overline>
      <List.Title><List.Action>api-04</List.Action></List.Title>
      <List.Description>Healthy for 41 days</List.Description>
    </List.Content>
    <List.End>
      <Span fontWeight={600}>99.98%</Span>
      <List.Meta>Uptime</List.Meta>
    </List.End>
  </List.Item>
</List>
```

## One action covers the item

`List.Action` goes in the title, and its `::after` stretches over the whole item — so the whole row presses, the focus ring draws round the whole row, and the name a reader hears is the title alone rather than every value in it. What it is follows from what it is given: an `href` makes it a link, an `htmlFor` makes it the label of a checkbox (a press anywhere on the item ticks it), and neither makes it a button.

A control of the item's own goes in List.Actions

A switch, a menu, a second button: `List.Actions` lifts them above the covering action, so they take their own presses. Anything outside it — media, text, the end values — presses the action.

A pressable item says so without a prop

The hover, pressed, current (`aria-current`) and disabled states are read off the action with `:has()`, so an item with no action is plain text and one with an action looks pressable — nothing to keep in step.

```jsx
<List variant="card">
  {people.map((person) => (
    <List.Item key={person.id} selected={picked.includes(person.id)}>
      <List.Actions>
        <Checkbox id={`pick-${person.id}`} checked={picked.includes(person.id)} onChange={() => toggle(person.id)} />
      </List.Actions>
      <List.Content>
        <List.Title><List.Action props={{ htmlFor: `pick-${person.id}` }}>{person.name}</List.Action></List.Title>
      </List.Content>
    </List.Item>
  ))}
  <List.Item>
    <List.Media variant="icon"><Icon size={5}><Bell /></Icon></List.Media>
    <List.Content>
      <List.Title><List.Action props={{ htmlFor: 'setting-notifications' }}>Notifications</List.Action></List.Title>
    </List.Content>
    <List.Actions>
      <Switch id="setting-notifications" checked={notifications} onChange={toggleNotifications} />
    </List.Actions>
  </List.Item>
</List>
```

## Four looks, three sizes

Dividers are the default — the right look for a long list of things of one kind, where a frame round every item is noise. `variant="plain"` drops them, `"card"` frames the whole list as one block (the settings screen), and `"separated"` frames each item on its own with a gap between. An item's `size` is `sm` (44px, still a finger's width), `md` (56px) or `lg` (72px).

```jsx
<List variant="separated">
  <List.Item size="sm">…</List.Item>
</List>
```

## Labelled values

`List.Fields` is a `<dl>` of `List.Field`s, each a label over its value, as many to a line as fit at 8rem — two on a phone. Every value keeps its label for a screen reader as well as for the eye, which a row of bare values does not.

```jsx
<List variant="card">
  <List.Item ai="start">
    <List.Content>
      <List.Overline>Order 4182</List.Overline>
      <List.Row>
        <List.Title lines={1}><List.Action>Grace Hopper</List.Action></List.Title>
        <List.End><Span fontWeight={600}>€42.00</Span><List.Meta>Total</List.Meta></List.End>
      </List.Row>
      <List.Fields>
        <List.Field label="Status">Paid</List.Field>
        <List.Field label="Items">3</List.Field>
        <List.Field label="Placed">15 Oct</List.Field>
        <List.Field label="Ships">Tomorrow</List.Field>
      </List.Fields>
    </List.Content>
  </List.Item>
</List>
```

## Groups, and a label that sticks

`List.Group` is an item holding its label and a list of its own, so a reader hears the label and then "list, 3 items". `level` makes the label a heading to jump between; `sticky` keeps it at the top of the screen while its items scroll under it, until the next group's label pushes it off — set `vars={{ 'list-sticky-top': '4rem' }}` on the list to clear an app's own header.

```jsx
<List variant="card">
  <List.Group label="Today" level={3} sticky>
    <List.Item>…</List.Item>
  </List.Group>
  <List.Group label="Earlier" level={3} sticky>
    <List.Item>…</List.Item>
  </List.Group>
</List>
```

## Swipe an item for its commands

`SwipeItem` (`@box-kite/react/components/swipeItem`) is a `List.Item` whose row slides along the line while a tray of `SwipeItem.Action`s opens in the space it leaves — `startActions` under the start edge, `endActions` under the end, the first of each outermost. Past half open it opens, short of it it closes, and a flick goes the way it was going; with `fullSwipe`, carrying it most of the way across performs the outermost action. Nothing is drawn under the row, so it keeps whatever the list paints. Try it with a finger, a pen or a mouse — or Tab to the actions, which opens the tray they are in.

```jsx
<List>
  <SwipeItem
    fullSwipe
    startActions={<SwipeItem.Action variant="accent" onClick={pin}>Pin</SwipeItem.Action>}
    endActions={
      <>
        <SwipeItem.Action variant="danger" onClick={remove}>Delete</SwipeItem.Action>
        <SwipeItem.Action onClick={snooze}>Snooze</SwipeItem.Action>
      </>
    }
  >
    <List.Content>
      <List.Title lines={1}>
        <List.Action props={{ href: '#ada' }}>Ada Lovelace</List.Action>
      </List.Title>
    </List.Content>
  </SwipeItem>
</List>
```

A swipe is a shortcut, not the only way in

The actions are real buttons in the tab order, and focus reaching one opens its tray. A path-based gesture still wants a way in for a pointer that cannot drag: put the same commands on the item's own page, or in a `Menu` in `List.Actions`.

An open item closes before it opens

A press on an open item closes it rather than following its action, and a press anywhere else closes it too — the press was most likely aimed at the tray.

## Put the items in order by hand

`SortableList` (`@box-kite/react/components/sortableList`) is a `List` whose items are dragged by a `SortableList.Handle`. The app owns the order: `onOrderChange` reports it as the items' `value`s, and the list draws whatever order it is rendered in. During a drag nothing is reordered — the item follows the pointer and its neighbours step aside — and the order changes once, on the drop, which glides into place. `onOrderCommit` is the one to save.

```jsx
<SortableList onOrderChange={(order) => setIds(order)}>
  {ids.map((id) => (
    <SortableList.Item key={id} value={id} label={names[id]}>
      <List.Content>
        <List.Title>{names[id]}</List.Title>
      </List.Content>
      <SortableList.Handle />
    </SortableList.Item>
  ))}
</SortableList>
```

The keyboard gets a grab

Enter or Space on a handle picks the item up, the arrows move it, Enter drops it and Escape puts it back — every step announced in a live region.

Or press, then press where it goes

A press on a handle that does not drag picks the item up too, and a press on another item then drops it there: the way to reorder with a pointer that cannot drag.

## A list too long to render whole

`VirtualList` (`@box-kite/react/components/virtualList`) renders the items near the screen and makes the rest space. Each item drawn is measured and kept by its `itemKey`, so items of different heights mix; one not drawn yet counts as the mean of those that have been, and the list holds its height while it scrolls, so a dragged scrollbar thumb stays under the pointer. The scroller is found rather than declared — the nearest one that scrolls, or the page — and each item carries `aria-setsize` and `aria-posinset`, so a reader hears "12 of 10,000". `onRangeChange` is where an endless list fetches its next page.

```jsx
<VirtualList items={messages} itemKey={(message) => message.id}>
  {(message) => (
    <List.Item>
      <List.Content>
        <List.Title lines={1}>{message.subject}</List.Title>
        <List.Description lines={message.long ? 2 : 1}>{message.text}</List.Description>
      </List.Content>
    </List.Item>
  )}
</VirtualList>
```

## What a reader hears

A `<ul role="list">` — the role spelled out, because Safari stops reporting a list once its items lose their markers — and `tag="ol"` when the order means something. A list is not a composite widget: each action is a tab stop of its own, and the arrow keys stay the page's. When the arrows should move between options and pick one, the pattern is a listbox, which is `Combobox` or `Select`.

## Styling

Every part is a Box, so every prop is available on it and beats the part's own default. The style tree, `Box.components('list')`, holds the paint and none of the spacing, because every engine entry carries it: `list` (variants `plain`, `card`, `separated`), `list.item` (the divider's colour, the pressed, hover and current states, and the `selected` variant), `list.media` (`icon`, `avatar`, `image`), `list.muted` (every quieter part), `list.action` (the focus ring) and `list.label` (a group's label). The hairlines and the tints are mixed from `currentColor`, so they follow any theme of yours with no rule for it. What the list's look says to its items travels as custom properties, because a list has no context to hand them down — and they are yours to set:

```jsx
<List
  vars={{
    'list-divider': 'slate-200',      // the line between items
    'list-divider-inset': '4.5rem',   // start it at the text, not the edge
    'list-item-radius': '0.5rem',     // round each item's tint
    'list-sticky-top': '4rem',        // clear a fixed header
  }}
>
```

## List props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The items: `List.Item`s, and `List.Group`s for sections. |
| `tag` | `'ul' \| 'ol'` | — | `ol` when the order means something — steps, a ranking. Default `ul`. |

### List.Item

One thing in the list: a flex row of its parts.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The item's parts: `List.Media`, `List.Content`, `List.End`, `List.Actions` — or anything at all, in one row. |
| `size` | `ListItemSize` | `'md'` | The item's spacing. Default `md`, at least 56px tall; `sm` is 44px, still a finger's width; `lg` is 72px. |
| `wrap` | `boolean` | `false` | Lets the item's parts wrap onto a second line when they do not fit, instead of squeezing. |
| `selected` | `boolean` | `false` | Tints the item as chosen. The state a reader hears belongs on the control that chose it — a checkbox, `aria-pressed`. |
| `chevron` | `boolean` | `false` | Ends the item with a chevron: pressing it goes somewhere with more. Decoration, so `aria-hidden`. |

### List.Media

The leading slot: an icon, an avatar, a thumbnail — `variant` is `icon`, `avatar` or `image`, or none for a bare icon.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.Content

The middle of an item, taking the room: a column of rows and text, which may shrink past its content.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.Row

A row inside `List.Content`: its parts side by side, the last at the end.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What sits side by side: a title and its time, a status and a count. The first takes the room, the last sits at the end. |
| `wrap` | `boolean` | `false` | Lets the row's parts wrap onto a new line — a set of tags — instead of pushing the last to the end. |
| `secondary` | `boolean` | `false` | The quieter line under the main one: smaller and muted. |

### List.Overline

A small line above the title: an id, a date, a category.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The text. |
| `lines` | `ListLines` | — | How many lines it runs to before it stops: `1` ends in an ellipsis, `2` and `3` are clamped. Unset, it wraps. |

### List.Title

The item's name, and where its `List.Action` goes.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The text. |
| `lines` | `ListLines` | — | How many lines it runs to before it stops: `1` ends in an ellipsis, `2` and `3` are clamped. Unset, it wraps. |

### List.Description

Muted supporting text under the title.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The text. |
| `lines` | `ListLines` | — | How many lines it runs to before it stops: `1` ends in an ellipsis, `2` and `3` are clamped. Unset, it wraps. |

### List.Meta

A small muted value that never wraps — a time, a count — in tabular figures.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.End

The trailing slot: a value and its label, stacked and aligned to the end so a column of them lines up.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.Actions

Controls of the item's own — a switch, a menu, a checkbox — lifted above the action that covers the item.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.Action

The item's one primary action, stretched over the whole item.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What the action is called: the item's title, and nothing else, since it is the name a reader hears. |
| `props` | `ListActionTagProps` | — | The element's attributes. An `href` makes it an `<a>`, an `htmlFor` a `<label>`; neither, a `<button type="button">`. |
| `tag` | `'button' \| 'a' \| 'label'` | — | The element, when what `props` holds does not already say: `a` with no `href` is a placeholder link. |
| `onClick` | `(event: React.MouseEvent<HTMLElement>) => void` | — | Fires on a press anywhere on the item — and on Enter, and on Space for a button. |

### List.Fields

A `<dl>` of `List.Field`s, as many to a line as fit at 8rem each.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

### List.Field

A labelled value: a `<dt>` and a `<dd>` in a `<div>`, which is the one wrapper a `<dl>` allows.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `label`required | `React.ReactNode` | — | What the value is: the `<dt>`. |
| `children` | `React.ReactNode` | — | The value: the `<dd>`. |

### List.Group

A section of the list: its label, then its own `<ul>` of items.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `label`required | `React.ReactNode` | — | What the items have in common: "Today", "A", "Unread". |
| `children` | `React.ReactNode` | — | The group's items. |
| `level` | `ListGroupLevel` | — | Makes the label a heading of this level, so a screen reader can jump between groups. Unset, it is plain text. |
| `sticky` | `boolean` | `false` | Keeps the label at the top of the screen while its items scroll under it, until the next group's pushes it off. |

### List.Chevron

The chevron that ends a drill-in item, pointing along the reading order. Decoration, so `aria-hidden`.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What goes in it. |

## List keyboard

| Key | Result |
| --- | --- |
| `Tab` | Each item's action, then each control in its `List.Actions`. |
| `Enter` | Follows the action's link or presses its button; Space presses a button. |

## List accessibility

- A `<ul role="list">`: the role is spelled out because Safari drops a list's semantics once its items lose their markers. `tag="ol"` makes it an ordered one.
- `List.Action`'s name is its own text, not the whole item's — a reader hears "Ada Lovelace, link", not every value in the row.
- A list is not a composite widget: every action is its own tab stop and the arrow keys stay the page's. For arrow-key selection among options, that is a listbox — `Combobox` or `Select`.

Swept with axe on every release, in this state: `List`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## List style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`list`variants: plain, card, separated

`list.item`variants: selected, lifted, grabbed, settling

`list.media`variants: icon, avatar, image

`list.muted`

`list.action`

`list.label`

## SwipeItem props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The item's parts, exactly as in a `List.Item`: `List.Media`, `List.Content`, `List.End`, `List.Actions`. |
| `startActions` | `React.ReactNode` | — | `SwipeItem.Action`s under the inline start edge — a swipe towards the end of the line reveals them. The first is outermost. |
| `endActions` | `React.ReactNode` | — | `SwipeItem.Action`s under the inline end edge — a swipe towards the start reveals them. The first is outermost. |
| `fullSwipe` | `boolean` | `false` | A swipe carried most of the way across performs the outermost action instead of stopping at the tray — iOS's archive. |
| `open` | `SwipeSide \| null` | — | Which tray is open. Controlled; `null` is closed. |
| `defaultOpen` | `SwipeSide \| null` | `null` | Which tray is open to begin with. |
| `onOpenChange` | `ChangeHandler<SwipeSide \| null, SwipeReason>` | — | Which tray is open now, and why: a `swipe`, `focus`, an `outside-pointer` press, `escape`, or an `action` chosen. |
| `size` | `ListItemSize` | `'md'` | The item's spacing, as on `List.Item`. |
| `wrap` | `boolean` | `false` | Lets the item's parts wrap onto a second line, as on `List.Item`. |
| `selected` | `boolean` | `false` | Tints the item as chosen, as on `List.Item`. |
| `chevron` | `boolean` | `false` | Ends the item with a chevron, as on `List.Item`. |

### SwipeItem.Action

One command in a tray: a label, and an icon over it if there is one.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What it does, in a word: "Archive", "Delete". It is the button's name. |
| `icon` | `React.ReactNode` | — | An icon over the label. Decoration — the label names the button. |
| `onClick` | `(event: React.MouseEvent<HTMLButtonElement>) => void` | — | Runs the command. The tray closes after it unless `closeOnSelect` is `false`. |
| `closeOnSelect` | `boolean` | `true` | Whether choosing the action closes the tray. Default `true`. |

## SwipeItem keyboard

| Key | Result |
| --- | --- |
| `Tab` | Reaches the tray's actions after the item's own, opening the tray they are in. |
| `Escape` | Closes an open tray and returns focus to the item's action. |

## SwipeItem accessibility

- A swipe is a shortcut, not the only way in. The actions are real buttons in the tab order, after the item's own action, and focus reaching one opens its tray — so a keyboard and a screen reader find them without swiping. A path-based gesture still needs a non-path way for a pointer (WCAG 2.5.1): offer the same commands where a press reaches them too, on the item's own page or in a `Menu` in `List.Actions`.

Swept with axe on every release, in this state: `SwipeItem`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## SwipeItem style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`swipeItem`

`swipeItem.row`variants: dragging

`swipeItem.tray`variants: dragging

`swipeItem.action`variants: accent, warning, danger, armed

## SortableList props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | The items: `SortableList.Item`s, each with a `value`, rendered in the order the app holds. |
| `onOrderChange` | `ChangeHandler<string[], SortableReason, SortableChangeDetails>` | — | The new order, as the items' `value`s. Fires once for a drop and on every step the keyboard takes, so the list moves as the arrows do. The app owns the order: render the items in it and the list follows. |
| `onOrderCommit` | `ChangeHandler<string[], SortableReason, SortableChangeDetails>` | — | The order once it has settled — a drop, or Enter after the arrows. The one to save. |
| `tag` | `'ul' \| 'ol'` | — | `ol` when the order means something — steps, a ranking. Default `ul`. |

### SortableList.Item

One item in the order: a `List.Item` that knows who it is.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `value`required | `string` | — | Who the item is in the order `onOrderChange` reports. Unique within the list. |
| `label` | `string` | — | What the handle and the announcements call the item: "Move Ada Lovelace". Defaults to `value`. |
| `disabled` | `boolean` | `false` | Leaves the item where it is: its handle is disabled. Others may still move past it. |
| `children` | `React.ReactNode` | — | The item's parts: `List.Media`, `List.Content`, `List.End`, `List.Actions` — or anything at all, in one row. |
| `size` | `ListItemSize` | — | The item's spacing. Default `md`, at least 56px tall; `sm` is 44px, still a finger's width; `lg` is 72px. |
| `wrap` | `boolean` | — | Lets the item's parts wrap onto a second line when they do not fit, instead of squeezing. |
| `selected` | `boolean` | — | Tints the item as chosen. The state a reader hears belongs on the control that chose it — a checkbox, `aria-pressed`. |
| `chevron` | `boolean` | — | Ends the item with a chevron: pressing it goes somewhere with more. Decoration, so `aria-hidden`. |

### SortableList.Handle

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | — | What the handle shows. Six dots unless given something else; its name is "Move" and the item's `label` either way. |

## SortableList keyboard

| Key | Result |
| --- | --- |
| `Enter, Space` | Picks the item up, and drops it again. |
| `ArrowUp, ArrowDown` | Moves a held item one place. Home and End move it to either end. |
| `Escape` | Puts a held item back where it was picked up from. |

## SortableList accessibility

- Dragging is not a keyboard gesture, so the keyboard gets a **grab**: Enter or Space on a handle picks the item up, the arrows move it, Enter drops it and Escape puts it back — DashboardGrid's model. Every step is announced in a polite live region that is there before there is anything to say.
- A press on a handle that does not drag picks the item up too, and while one is held a press on any other item drops it there — the single-pointer way to reorder that WCAG 2.5.7 asks a drag to have.

Swept with axe on every release, in this state: `SortableList`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## SortableList style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`sortableList`

`sortableList.handle`variants: grabbed

## VirtualList props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `items`required | `readonly TItem[]` | — | Every item, however many — only the ones near the screen are rendered. |
| `itemKey`required | `(item: TItem, index: number) => Key` | — | Who an item is: stable across renders, since an item's measured height is kept under it. |
| `children`required | `(item: TItem, index: number) => React.ReactNode` | — | Draws one item: a `List.Item`, a `SwipeItem` — one `<li>` per item, and nothing around it. |
| `estimateSize` | `number` | `57` | What an item counts as until one has been drawn, in px — then the mean of those drawn. Default 57, a `md` item. |
| `overscan` | `number` | `6` | Items kept either side of what is on screen, so a fast scroll has something to show. Default 6. |
| `initialCount` | `number` | `20` | How many items render before anything is measured — on a server, and in the first frame. Default 20. |
| `onRangeChange` | `(range: VirtualListRange) => void` | — | Fires when the rendered range moves. The way to fetch more: load the next page when `endIndex` nears the end. |
| `scrollerRef` | `React.RefObject<HTMLElement \| null>` | — | What scrolls the list, when it is not the nearest scrolling ancestor — or, with none, the page. |
| `tag` | `'ul' \| 'ol'` | — | `ol` when the order means something — steps, a ranking. Default `ul`. |

## VirtualList accessibility

- Each rendered item carries `aria-setsize` and `aria-posinset`, so a screen reader says "12 of 4,000" rather than counting the twenty in the DOM.
- The item focus is in stays rendered while the list scrolls away from it, so Tab carries on from where it was; and since Tab scrolls its target into view, the window follows the keyboard down the list.
- Find-in-page cannot see an item that is not rendered. A list that has to be searchable that way is one to render whole — or to give a search field of its own.

Swept with axe on every release, in this state: `VirtualList`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## VirtualList style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`list`variants: plain, card, separated

`list.item`variants: selected, lifted, grabbed, settling

`list.media`variants: icon, avatar, image

`list.muted`

`list.action`

`list.label`

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

