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.
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
single — one row at a time
multiple — a checkbox per row
// 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.
Secondary
The secondary variant drops that wrapper, leaving transparent rows divided by hairline borders and an accent-tinted selection.
Disabled Items
disabledKeys takes rows out of selection, keyboard navigation, and pointer interaction while leaving them visible.
// 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.
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, andoutline-style: none. It carries no fill of its own — that is the variant's job — and gains[data-virtualized]whenvirtualizedis set, which every row rule below reads..list-view--primary—padding: --spacing * 1,border-radius: --radius * 2.5, andbackground-color: --surface-secondary, so the rows sit inset on a card-shaped surface. Its rows take their own--surfacefill, and the first and last round to--radius * 2— a pair of rules deliberately skipped when virtualized, where the root takesoverflow: hiddeninstead, 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: flexwithalign-items: center, a--spacing * 3gap,--spacing * 4inline /--spacing * 3block padding, 0.875rem / 1.25rem text,cursor: var(--cursor-interactive), and-webkit-tap-highlight-color: transparent. Both variants addborder-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 whenselectionModeis notnoneandselectionBehavioristoggle, so areplacelist has no checkbox column at all..list-view__item-content—flex: 1with a--spacing * 3gap andmin-width: 0, which is what makes this the column that yields when the row runs out of room. A descendantsvgis forced to--spacing * 4on 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, withoverflow: hidden,text-overflow: ellipsis, andwhite-space: nowrap..list-view__description— 0.75rem / 1rem in--mutedwithmargin-top: --spacing * 0.5, truncated the same way..list-view__item-action—margin-left: autopins it to the trailing edge,flex-shrink: 0keeps it at its natural size while the content column absorbs the squeeze, andpadding-left: --spacing * 2holds it off the content..list-view__empty-state— The wrapper the component puts aroundrenderEmptyStateoutput. Centred both ways,--spacing * 4inline /--spacing * 8block padding..list-view__load-more— A centred,--spacing * 3block-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
- Hover —
color-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. - Selected —
color-mix(in oklab, var(--surface) 10%, transparent)on primary;var(--accent-soft)on secondary, which deepens tovar(--accent-soft-hover)when a selected row is also hovered. - Focus visible —
box-shadow: inset 0 0 0 2px var(--focus)withoutline-style: none, on both:focus-visibleand[data-focus-visible="true"], so the ring lands inside the row rather than outside its bottom border. - Disabled —
opacity: var(--disabled-opacity),cursor: var(--cursor-disabled), andpointer-events: none. - Dragging —
[data-dragging="true"]drops the row toopacity: 0.5. - Motion — rows transition
background-color, opacityover 120ms onvar(--ease-out). The press is asymmetric:transition-duration: 0swhile: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.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | The 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. |
selectedKeys | Selection | — | Controlled selection. A Set of keys, or the literal "all" — handle that second form when you read it, because it does not carry the ids. |
defaultSelectedKeys | Selection | — | Initial selection for an uncontrolled list. |
onSelectionChange | (keys: Selection) => void | — | Fires 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. |
items | Iterable<T> | — | The data for a dynamic collection. Pair it with a function children; RAC then re-renders only the rows whose item changed. |
disabledKeys | Iterable<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) => void | — | Fires 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 | () => ReactNode | — | What 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. |
virtualized | boolean | false | Wraps 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. |
rowHeight | number | 48 | The 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. |
className | string | ((values) => string) | — | Merged onto .list-view and its variant modifier. The render-prop form receives RAC's list render props. |
children | ReactNode | ((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.
| Prop | Type | Default | Description |
|---|---|---|---|
id | Key | — | The 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. |
textValue | string | — | The 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. |
isDisabled | boolean | — | Disables this one row, with the same effect as naming it in the list's disabledKeys. Use whichever is closer to where the state lives. |
href | string | — | Renders 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. |
className | string | ((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. |
children | ReactNode | — | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The 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. |
className | string | — | Merged onto .list-view__item-content. |
Also accepts every native div attribute.
ListView.Title
The row's primary line. A span.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The 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. |
className | string | — | Merged onto .list-view__title. |
Also accepts every native span attribute.
ListView.Description
The row's supporting line, in --muted. A span.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The secondary text. Truncates to one line like the title, so it suits metadata — a timestamp, an owner, a size — rather than prose. |
className | string | — | Merged 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A 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. |
className | string | — | Merged onto .list-view__item-action. |
Also accepts every native div attribute.