---
title: List
description: Settings and records as rows with a label, a description and a control on the right.
---

```tsx
import { List, Switch } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ListBasic() {
  return (
    <Preview block={true}>
      <List.Root>
        {Array.from({ length: 2 }, (_, index) => (
          <List.Field description="Description" key={index} label={`Label ${index + 1}`}>
            <Switch.Root aria-label={`Label ${index + 1}`} />
          </List.Field>
        ))}
      </List.Root>
    </Preview>
  );
}
```

- Use a list for settings and for short collections people manage one by one: members, tokens, webhooks. Use [Table](/deft/components/table) when people scan and compare many rows.
- `List.Field` pairs one control with its label and description. Pass `htmlFor` with the control's `id` so the label is a `label`.
- The label names the setting; the description says what it changes, in one sentence.
- The list sits on the inset background, like a panel, and rows are split by hairline dividers.
- Give inputs, textareas, selects and buttons inside a list `variant="field"`, so they stand out from the inset background.
- `layout="inline"`, the default, keeps the control on the label's line at any width, in a fixed column on the right, or the right half of the row in a narrow list. Use it for switches, buttons, short text and fields without a description.
- `layout="wrap"` keeps the control on the right in wide lists and moves it under the label at full width in narrow ones. Use it for an input or select next to a description, or for a group of controls.
- `layout="stacked"` always puts the control under the label, for wide controls such as a textarea.
- `List.Empty` holds the empty state inside the list. Fill it like an [Empty](/deft/components/empty).

## Records

```tsx
import { Button, List } from "@openperf/deft";
import { IconDots, IconPlus } from "@tabler/icons-react";

import { Preview } from "../../src/Preview";

export default function ListRecords() {
  return (
    <Preview block={true}>
      <List.Root>
        {Array.from({ length: 2 }, (_, index) => (
          <List.Item key={index}>
            <List.Icon variant="tile">
              <IconPlus />
            </List.Icon>
            <List.Header>
              <List.Label>Item {index + 1}</List.Label>
              <List.Description>Description</List.Description>
            </List.Header>
            <List.Actions>
              <Button.Root aria-label="Options" shape="square" size="sm" variant="ghost">
                <IconDots />
              </Button.Root>
            </List.Actions>
          </List.Item>
        ))}
      </List.Root>
    </Preview>
  );
}
```

Compose `List.Item` from `Icon`, `Header` with `Label` and `Description`, and `Actions`. Give the leading icon `variant="tile"` so every list uses the same tile; avatars go in a tile with `shape="circle"`, which sizes the avatar to fill it. Put a row's secondary actions in `List.Actions`, behind a ghost icon button at the end.

## Links

```tsx
import { List } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ListLinks() {
  return (
    <Preview block={true}>
      <List.Root>
        {Array.from({ length: 2 }, (_, index) => (
          <List.Item key={index} render={<a href={`#item-${index + 1}`} />}>
            <List.Header>
              <List.Label>Item {index + 1}</List.Label>
            </List.Header>
            <List.Chevron />
          </List.Item>
        ))}
      </List.Root>
    </Preview>
  );
}
```

A row that opens a detail page renders as a link through `render` and ends with `List.Chevron`. The chevron is decorative (`aria-hidden`) and nudges right on row hover. A status such as a [Dot](/deft/components/dot) goes between the header and the chevron. Rows without a page keep their action in `List.Actions` and have no chevron.

## Buttons

```tsx
import { Badge, List } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ListButtons() {
  return (
    <Preview block={true}>
      <List.Root>
        <List.Item render={<button type="button" />}>
          <List.Header>
            <List.Label>Option 1</List.Label>
          </List.Header>
          <List.Chevron />
        </List.Item>
        <List.Item render={<button disabled={true} type="button" />}>
          <List.Header>
            <List.Label>Option 2</List.Label>
            <List.Description>Description</List.Description>
          </List.Header>
          <Badge.Root>Disabled</Badge.Root>
        </List.Item>
      </List.Root>
    </Preview>
  );
}
```

A row that picks an option without opening a page renders as a `button` through `render`. It takes the full width and gets the same hover and focus ring as a link. A disabled button keeps its look but loses the hover, so explain why in `List.Description` and show the state with a [Badge](/deft/components/badge) in place of the chevron.

## API

| Prop | Type | Default | Description |
| - | - | - | - |
| `layout?` | `"inline" \| "wrap" \| "stacked"` | `"inline"` | On Item and Field. |
| `render?` | `ReactElement \| function` | - | On Item and Label. Renders a link or a button in place of the `div`. |
| `label` | `ReactNode` | - | On Field. |
| `description?` | `ReactNode` | - | On Field. |
| `htmlFor?` | `string` | - | On Field and Label. Renders the label as a `label`. |
| `variant?` | `"plain" \| "tile"` | `"plain"` | On Icon. |
| `shape?` | `"square" \| "circle"` | `"square"` | On Icon, for tiles. |

Parts: `Root`, `Actions`, `Chevron`, `Control`, `Description`, `Empty`, `Field`, `Header`, `Icon`, `Item`, `Label`. `Field` takes the `Item` props and renders `Header`, `Label`, `Description` and `Control` itself. Other props go to the native element.
