Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

List

Settings and records as rows with a label, a description and a control on the right.

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 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.

Records

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.

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 goes between the header and the chevron. Rows without a page keep their action in List.Actions and have no chevron.

Buttons

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 in place of the chevron.

API

PropType
layout?"inline" | "wrap" | "stacked"

On Item and Field.

Type"inline" | "wrap" | "stacked"
Default"inline"
render?ReactElement | function

On Item and Label. Renders a link or a button in place of the `div`.

TypeReactElement | function
labelReactNode

On Field.

TypeReactNode
description?ReactNode

On Field.

TypeReactNode
htmlFor?string

On Field and Label. Renders the label as a `label`.

Typestring
variant?"plain" | "tile"

On Icon.

Type"plain" | "tile"
Default"plain"
shape?"square" | "circle"

On Icon, for tiles.

Type"square" | "circle"
Default"square"

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.

Was this page helpful?