List

A vertical list of connected rows, each with a title, a detail and an action.

StableSince 1.0.0
On this page

Import

import { List } from '@envsemble/envui';

Usage

List.Root is a ul of List.Item rows, each with its label, a description below it, one short fact, content at its end, such as a Button, and children after the description, such as a Badge. start holds content before the label, such as an Avatar, an icon or a Checkbox, in a slot at least an icon wide, so a narrower mark, such as a Spinner, keeps its label in line with its siblings'. Name the list with aria-label where its heading doesn't.

SettingsRows.tsx
import { Button, List } from '@envsemble/envui';

export default function SettingsRows() {
  return (
    <List.Root aria-label="Workspace settings">
      <List.Item
        label="Notifications"
        description="E-mail and in-app alerts"
        end={<Button variant="outline">Manage</Button>}
      />
      <List.Item
        label="Members"
        description="12 people · 3 pending invitations"
        end={<Button variant="outline">Manage</Button>}
      />
      <List.Item label="Plan" description="Team, renews on 1 December" />
    </List.Root>
  );
}

Rows that open something

With interactive, a row reacts to hover, keyboard focus inside it and presses. Mark its one link or button data-row-target, which stretches it over the row; give a link draggable={false}, so a press that moves a little doesn't drag it. The row's other controls take position: relative to stay above it. While the target has keyboard focus, its ring traces the row's edge, the area a press follows.

<List.Item
  interactive
  label={
    <Link href="/plans/roadmap" underline={false} data-row-target draggable={false}>
      Roadmap
    </Link>
  }
  description="Updated today by Sam Lee"
  end={
    <IconButton
      variant="text"
      icon={<IconMoreHoriz />}
      aria-label="More actions for Roadmap"
      style={{ position: 'relative' }}
    />
  }
/>

The current and checked rows

current marks the current row, such as the one whose details show beside the list, with the selected fill and aria-current="true" on its data-row-target, or on the row without one. A row whose start checkbox is checked takes the selected fill too; name each checkbox after its row.

<List.Item interactive current={id === shown} label="Roadmap" />

Put in order

onReorder on List.Root gives each row a handle, moved by dragging it, by pressing it or by keyboard, and calls it with the row's index and the index it is dropped at. The application moves the item in its own state; nothing moves until it does. The rows are the list's children, directly or in arrays and fragments: List.Item elements, or elements of a row component that passes the props it doesn't use on to its List.Item. textValue names a row whose label isn't plain text, for its handle and the announcements.

<List.Root
  aria-label="Agenda"
  onReorder={(from, to) =>
    setItems((list) => {
      const next = [...list];
      const [item] = next.splice(from, 1);
      next.splice(to, 0, item!);
      return next;
    })
  }
>
  {items.map(({ id, title, length }) => (
    <List.Item key={id} label={title} description={length} />
  ))}
</List.Root>

Density and panes

density is xs, sm, md, the default, or lg, and changes only the rows' block padding. Put a list directly in <Surface background="muted" padding={3}>, a muted Surface that holds only lists, and its rows take the raised fill.

<Surface background="muted" padding={3}>
  <List.Root aria-label="Notifications" density="sm">
    …
  </List.Root>
</Surface>

Composition

The parts are the list, env-list, each row, env-list-item, with its handle, env-list-item-handle, its start, env-list-item-start, its content, env-list-item-content, which holds the label and the description, and its action at the end. Rows render on the server; only the handle of a reorderable row needs the browser.

Accessibility

You provide

  • A name for the list where its heading doesn't give one.
  • One link or button marked data-row-target in an interactive row, with draggable={false} on a link.
  • Names for a start checkbox and for actions, which say which row they act on.
  • textValue on a reorderable row whose label isn't plain text, and the move in onReorder.

envUI provides

  • A native ul of li rows, announced with their count.
  • aria-current="true" on the current row's target.
  • Handles named Reorder and the row's label, described by how to move without a pointer.
  • A polite announcement of each step of a move, with the position and the count.

Props

List.Root

A `ul` of connected rows with shared outer corners. In a muted Surface that holds only lists, such as `<Surface background="muted" padding={3}>`, the rows take the raised fill, so they stand out from it. With onReorder, people move the rows by their handles.

PropTypeDescription
densityDefault 'md'
'xs''sm''md''lg'
How tightly the rows pack: xs, sm, md or lg changes only their block padding, never the text.
onReorder
(from: number, to: number) => void
Makes the rows reorderable: each row gets a drag handle at its start, moved by dragging it, by pressing it or by keyboard, and the list calls this with the row's index and the index it is dropped at. The application moves the item in its own state; nothing moves until it does. The rows are the list's children, directly or in arrays and fragments: List.Item elements, or elements of a row component that passes the props it doesn't use on to its List.Item, such as `function SlideRow({ slide, ...props }) { return <List.Item {...props} label={slide} />; }`.
skeletonDefault false
boolean
Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew.

Also accepts the native props of <ul>, such as ref, className and aria-* attributes.

List.Item

One `li` row with a label, a description and content at its end; the current row, content at its start, and in a reorderable list a drag handle. It renders on the server: only the handle of a reorderable row needs the browser.

PropTypeDescription
labelrequired
ReactNode
The row's main text, such as an item's name; a link marked `data-row-target` in an interactive row. Undefined, such as over a placeholder item, a skeleton shows a bar in its place.
description
ReactNode
A short fact in muted text below the label, such as who changed the item and when. Given as undefined, such as over a placeholder item, a skeleton shows a bar in its place.
end
ReactNode
Content at the end of the row, such as a Button or a Switch; it moves below the text when it doesn't fit.
children
ReactNode
More content after the label and the description, such as a Badge.
interactiveDefault false
boolean
The row opens something through a link or button inside: it reacts to hover, keyboard focus inside it and presses. Mark that link or button `data-row-target` to stretch it over the row.
currentDefault false
boolean
The current row, such as the one whose details show beside a list: the selected fill, and `aria-current="true"` on its `data-row-target` element in the label, description or children, or on the row without one.
start
ReactNode
Content before the label, such as an Avatar, an icon or a Checkbox for choosing several rows; a row whose checkbox is checked takes the selected fill.
textValueDefault the label's text
string
The row's label as plain text, for its drag handle's name and the reorder announcements where `label` isn't a string.
skeletonDefault false
boolean
Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew.

Also accepts the native props of <li>, such as ref, className and aria-* attributes.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

env-list
The list.
env-list-announcer
The announcements of a reordered item.
env-list-item
One item.
env-list-item-content
An item's content.
env-list-item-handle
The handle that reorders an item.
env-list-item-start
The start of an item, for the handle or a checkbox.
data-density
lg · md · sm · xs
data-env-skeleton
Any value, or present
data-interactive
Present on env-list-item
data-label
Any value, or present on env-list-item-handle
data-moving
keyboard · pointer on env-list-item
data-reorderable
Present
data-reordering
Present
data-selected
Present on env-list-item
data-start
Present on env-list-item

Color properties

Each sets a color of one instance, through style or CSS; its hover, focus and press follow it. They are public and versioned.

--env-list-item-container
The row's fill; the state layers lie over it.
--env-list-item-content
The text, and the color of the state layers.
--env-list-item-selected
The fill of the current row and of a row whose checkbox is checked.
--env-list-item-selected-content
The text of a selected row.

Strings

EnvUIProvider's messages overrides each, and a component's own label prop wins over it.

messages.list.reorder
({ label }) => string A row's drag handle, such as Reorder Intro slide.
messages.list.instructions
"Press Space or Enter, or click, to pick the row up. Then use the arrow keys to move it, and press Space or Enter, or click, to drop it. Escape cancels." Describes each drag handle: how to move its row without a pointer.
messages.list.lifted
({ label, position, count }) => string Announced when a row is picked up by keyboard or a press of its handle; the handle's description already says how to move it.
messages.list.liftedByPointer
({ label, position, count }) => string Announced when the pointer starts dragging a row.
messages.list.moved
({ label, position, count }) => string Announced as a picked-up row moves.
messages.list.atStart
({ label, position, count }) => string Announced when a row can't move further up: it is first.
messages.list.atEnd
({ label, position, count }) => string Announced when a row can't move further down: it is last.
messages.list.dropped
({ label, position, count }) => string Announced when a row is dropped in its new place.
messages.list.cancelled
({ label, position, count }) => string Announced when a move is cancelled and the row goes back.

Changelog

1.0.0

List.Item takes current for the current row, marked in the server's HTML, start for content before the label, such as an avatar or a checkbox, and textValue; List.Root takes onReorder, which moves rows by a drag handle, by keyboard, by a press and by pointer, with announcements. GroupedRows is List.Root and Row is List.Item, with the env-list and env-list-item classes; the page is list. List.Item's title is label, its detail is description and its action is end, as on CommandPalette's items and Button. List.Root's contrast is gone: put the list directly in <Surface background="muted" padding={3}>, where its rows take the raised fill.

1.0.0-prerelease.6

A pressed row's link or button, data-row-target, lays no pressed fill of its own: the row shows the press.

Colors under the pointer and while pressed change with --env-motion-ease-enter, rather than easing in slowly, so hover and press show from the first frame.

In an interactive row the focus ring of the data-row-target link or button traces the row, not the label alone.

The start slot is at least an icon wide, with its content centred, so a Spinner among icons keeps its label in line.

1.0.0-prerelease.5

List.Item's selected is current, as it marks the current row with aria-current; selected means chosen elsewhere.