List View

A single-column interactive list with keyboard navigation, selection, and accessible item actions, built on RAC GridList.

Usage

ListView renders a single-column collection of rows with keyboard navigation, typeahead, and selection already wired up.

DocumentsUpdated 2 days ago
Budget.xlsxUpdated 5 days ago
Archived.zipUpdated 3 weeks ago
PhotosUpdated 1 month ago

Selection Modes

selectionMode decides whether rows are read-only, hold one selection at a time, or take a checkbox each.

none — rows act, nothing is selectable

Inbox12 unread
Drafts3 saved
SentJust now

single — one row at a time

Inbox12 unread
Drafts3 saved
SentJust now

multiple — a checkbox per row

Inbox12 unread
Drafts3 saved
SentJust now
// none — rows can still be actioned, but nothing is selectable
<ListView aria-label="Mailboxes" selectionMode="none" />

// single — one row at a time
<ListView aria-label="Mailboxes" selectionMode="single" defaultSelectedKeys={["drafts"]} />

// multiple — a checkbox per row
<ListView aria-label="Mailboxes" selectionMode="multiple" defaultSelectedKeys={["drafts", "sent"]} />

Variants

Primary

The default variant seats the rows on a card-shaped secondary surface, each row painted with its own --surface fill.

Design systemNadia Ferreira
Live
Marketing siteTom Okafor
Review
Mobile appPriya Patel
Live
API gatewayLuca Bianchi
Paused

Secondary

The secondary variant drops that wrapper, leaving transparent rows divided by hairline borders and an accent-tinted selection.

Design systemNadia Ferreira
Live
Marketing siteTom Okafor
Review
Mobile appPriya Patel
Live
API gatewayLuca Bianchi
Paused

Disabled Items

disabledKeys takes rows out of selection, keyboard navigation, and pointer interaction while leaving them visible.

SlackPost deploys to a channel
GitHubLink commits to issues
FigmaEnterprise plan only
SentryAttach traces to alerts
SalesforceEnterprise plan only
// Disable rows from the list, by key
<ListView aria-label="Integrations" disabledKeys={["figma", "salesforce"]} selectionMode="multiple">
  {INTEGRATIONS.map((integration) => (
    <ListView.Item key={integration.id} id={integration.id} textValue={integration.title} />
  ))}
</ListView>

// Or disable a single row from the row itself
<ListView.Item id="figma" isDisabled textValue="Figma" />

With Dock

Dock is our floating action bar for a selection — drive its isOpen from the number of selected keys and it animates in carrying the count and the bulk actions.

Brand kit12 files · 48 MB
Product shots84 files · 1.2 GB
Launch video.mp41 file · 340 MB
Press release.docx1 file · 82 KB
Pricing sheet.xlsx1 file · 24 KB

Anatomy

Import the ListView component and access all parts using dot notation.

import { ListView } from "@blakeui/pro-react";

<ListView>
  <ListView.Item>
    <ListView.ItemContent>
      <ListView.Title />
      <ListView.Description />
    </ListView.ItemContent>
    <ListView.ItemAction />
  </ListView.Item>
</ListView>;

CSS Classes

Base Classes

  • .list-view — The root. position: relative, width: 100%, min-height: 0, and outline-style: none. It carries no fill of its own — that is the variant's job — and gains [data-virtualized] when virtualized is set, which every row rule below reads.
  • .list-view--primarypadding: --spacing * 1, border-radius: --radius * 2.5, and background-color: --surface-secondary, so the rows sit inset on a card-shaped surface. Its rows take their own --surface fill, and the first and last round to --radius * 2 — a pair of rules deliberately skipped when virtualized, where the root takes overflow: hidden instead, because the virtualizer recycles rows and no single element stays first or last.
  • .list-view--secondary — No padding, no radius, no fill. Its rows are transparent and divided by the same hairline border, so the list reads as part of whatever surface already holds it.

Element Classes

  • .list-view__item — The row. display: flex with align-items: center, a --spacing * 3 gap, --spacing * 4 inline / --spacing * 3 block padding, 0.875rem / 1.25rem text, cursor: var(--cursor-interactive), and -webkit-tap-highlight-color: transparent. Both variants add border-bottom: 1px solid color-mix(in oklab, var(--separator-tertiary) 50%, transparent), dropped on :last-child.
  • .list-view__selection-cell — The checkbox holder, flex-shrink: 0. Rendered only when selectionMode is not none and selectionBehavior is toggle, so a replace list has no checkbox column at all.
  • .list-view__item-contentflex: 1 with a --spacing * 3 gap and min-width: 0, which is what makes this the column that yields when the row runs out of room. A descendant svg is forced to --spacing * 4 on both axes in --muted, so the leading icon's size is the slot's decision rather than the icon's.
  • .list-view__title — 0.875rem / 1.25rem at weight 500 in --foreground, with overflow: hidden, text-overflow: ellipsis, and white-space: nowrap.
  • .list-view__description — 0.75rem / 1rem in --muted with margin-top: --spacing * 0.5, truncated the same way.
  • .list-view__item-actionmargin-left: auto pins it to the trailing edge, flex-shrink: 0 keeps it at its natural size while the content column absorbs the squeeze, and padding-left: --spacing * 2 holds it off the content.
  • .list-view__empty-state — The wrapper the component puts around renderEmptyState output. Centred both ways, --spacing * 4 inline / --spacing * 8 block padding.
  • .list-view__load-more — A centred, --spacing * 3 block-padded footer row for a load-more affordance. It is declared in the stylesheet but no part emits it, so apply it yourself to whatever you render below the list.

Interactive States

  • Hovercolor-mix(in oklab, var(--surface) 40%, transparent) on primary, color-mix(in oklab, var(--default) 50%, transparent) on secondary. Both sit inside @media (hover: hover), so a touch device never leaves a hover fill stuck on the last row tapped.
  • Selectedcolor-mix(in oklab, var(--surface) 10%, transparent) on primary; var(--accent-soft) on secondary, which deepens to var(--accent-soft-hover) when a selected row is also hovered.
  • Focus visiblebox-shadow: inset 0 0 0 2px var(--focus) with outline-style: none, on both :focus-visible and [data-focus-visible="true"], so the ring lands inside the row rather than outside its bottom border.
  • Disabledopacity: var(--disabled-opacity), cursor: var(--cursor-disabled), and pointer-events: none.
  • Dragging[data-dragging="true"] drops the row to opacity: 0.5.
  • Motion — rows transition background-color, opacity over 120ms on var(--ease-out). The press is asymmetric: transition-duration: 0s while :active / [data-pressed="true"] so the press-in snaps and only the release eases back. Colour and opacity only — nothing moves — so reduced motion keeps the transition untouched and it needs no opt-out. Every row rule is scoped :not([data-virtualized]), because a recycled row would otherwise animate between two unrelated rows' states as you scroll.

API Reference

ListView

The root. A RAC GridList with the variant's classes applied, wrapped in a Virtualizer when virtualized is set.

PropTypeDefaultDescription
aria-labelstringThe list's accessible name. Required, unless you point aria-labelledby at a visible heading instead — which is what the Selection Modes demo above does, so each list is named by the caption a sighted reader is already using.
variant"primary" | "secondary""primary"Which surface model the list paints. primary is the inset card; secondary is transparent rows on the surface you already have. It also flips the row checkbox to the opposite variant, so the control keeps its contrast either way.
selectionMode"none" | "single" | "multiple""none"Whether rows are selectable, and how many at once. none is passed to RAC as undefined rather than the string, so a read-only list keeps a plain list role and never renders the selection column.
selectedKeysSelectionControlled selection. A Set of keys, or the literal "all" — handle that second form when you read it, because it does not carry the ids.
defaultSelectedKeysSelectionInitial selection for an uncontrolled list.
onSelectionChange(keys: Selection) => voidFires whenever the selection changes, by pointer or by keyboard. This is what a Dock hangs off — see With Dock above.
selectionBehavior"toggle" | "replace""toggle"The interaction model. toggle gives every row a checkbox and treats a click as a selection. replace is highlight-style: clicking a row replaces the selection, modifier-clicking extends it, and no checkbox column is rendered at all.
itemsIterable<T>The data for a dynamic collection. Pair it with a function children; RAC then re-renders only the rows whose item changed.
disabledKeysIterable<Key>Keys to disable. Disabled rows stay visible and stay in the reading order, but drop out of selection, out of arrow-key and typeahead navigation, and out of pointer interaction.
onAction(key: Key) => voidFires when a row is actioned — Enter, or a click on a list that is not in toggle selection. It is the hook for "open this row", separate from selecting it.
renderEmptyState() => ReactNodeWhat to render when the collection is empty. The return value is wrapped in .list-view__empty-state, which centres it and gives it its own padding, so return the content rather than a positioned container. Interactive content works — delete every row in the With Dock demo and the empty state's own button brings them back.
virtualizedbooleanfalseWraps the list in a RAC Virtualizer with a ListLayout, so only the visible rows are in the DOM. It also stamps [data-virtualized] on the root, which switches off the row transitions and the first/last row rounding — both are wrong on a recycled element.
rowHeightnumber48The estimated row height the virtualizer lays out against. Read only when virtualized is set. A row carrying both a title and a description measures taller than the default, so pass your real height or the scrollbar will drift.
classNamestring | ((values) => string)Merged onto .list-view and its variant modifier. The render-prop form receives RAC's list render props.
childrenReactNode | ((item: T) => ReactNode)Static ListView.Items, or a render function used with items.

Also accepts every RAC GridListProps.

ListView.Item

A row. A RAC GridListItem that renders the selection checkbox itself, ahead of whatever you pass.

PropTypeDefaultDescription
idKeyThe row's key — what turns up in selectedKeys, disabledKeys, and onAction. Required on a static list; supplied by the item's own id in a dynamic one.
textValuestringThe row's string form, used for typeahead and for the accessible name. Set it whenever the row is more than plain text — which, with an icon and a description in it, is essentially always.
isDisabledbooleanDisables this one row, with the same effect as naming it in the list's disabledKeys. Use whichever is closer to where the state lives.
hrefstringRenders the row as a link and navigates on action. For client-side routing, wrap the app in a RAC RouterProvider; without one it is an ordinary document navigation.
classNamestring | ((values) => string)Merged onto .list-view__item. The render-prop form receives RAC's item render props (isSelected, isHovered, isDisabled, and the rest), so per-state styling does not need a separate wrapper.
childrenReactNodeThe row's content — normally a ListView.ItemContent and, optionally, a ListView.ItemAction. The selection checkbox is not yours to render; the component adds it in front of these when selection is on.

Also accepts every RAC GridListItemProps.

ListView.ItemContent

The row's main column — leading icon or avatar, plus the text. A div.

PropTypeDefaultDescription
childrenReactNodeThe leading glyph and the text. Because this part is a flex row, a title and a description need a column wrapper between them and it — every demo on this page uses <div className="flex min-w-0 flex-col">, and the min-width: 0 is what lets the two lines truncate.
classNamestringMerged onto .list-view__item-content.

Also accepts every native div attribute.

ListView.Title

The row's primary line. A span.

PropTypeDefaultDescription
childrenReactNodeThe row's label. It truncates to a single line, so give the row a textValue as well — that, not this, is what typeahead and assistive tech read.
classNamestringMerged onto .list-view__title.

Also accepts every native span attribute.

ListView.Description

The row's supporting line, in --muted. A span.

PropTypeDefaultDescription
childrenReactNodeThe secondary text. Truncates to one line like the title, so it suits metadata — a timestamp, an owner, a size — rather than prose.
classNamestringMerged onto .list-view__description.

Also accepts every native span attribute.

ListView.ItemAction

The row's trailing slot. A div, pinned to the end of the row.

PropTypeDefaultDescription
childrenReactNodeA button, a menu, a chip, or a status glyph. The part has no layout of its own beyond the trailing pin, so for more than one child add your own className="flex items-center gap-2" — the With Dock demo does exactly that to sit a chip beside a star.
classNamestringMerged onto .list-view__item-action.

Also accepts every native div attribute.

On this page