Skip to content
ShineUI
Install

List

The master list of a list-and-detail screen.

<sui-list>DataStable

About

The master list of a list-and-detail screen: rich rows with a title, description, time, status or avatar disc and unread dot, one selected row, and a keyboard that walks them. It paints rows only, so it sits inside a card, a splitter pane or a popover without a second frame. Long titles truncate or wrap; rows never scroll sideways.

Usage

<sui-list aria-label="Notifications" selection="n2"></sui-list> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-list"); el.items = [ { section: "Today" }, { id: "n1", title: "Priya asked for a review on LRK-118", description: "Payment retries: back off after the third failure", meta: "11:02 AM", severity: "info", unread: true, }, { id: "n2", title: "LRK-104 is blocked", description: "Waiting on the vendor sandbox credentials", meta: "9:15 AM", severity: "attention", unread: true }, { id: "n3", title: "LRK-097 is overdue", description: "Due yesterday, assigned to you", meta: "8:40 AM", severity: "critical", unread: true }, { section: "Yesterday" }, { id: "n4", title: "Onboarding flow shipped", description: "Dana closed the last task in the cycle", meta: "Yesterday", severity: "positive" }, { id: "n5", title: "Weekly digest", description: "14 tasks closed, 3 opened", meta: "Yesterday", icon: "fa-regular fa-envelope" }, { id: "n6", title: "Archived workspace: Sandbox", description: "Nothing has changed here in 90 days", meta: "Aug 29", disabled: true }, ]; /* SuiListEntry[] */ el.addEventListener("suiItemPick", (e) => console.log(e.detail)); </script>
import { SuiList } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { section: "Today" }, { id: "n1", title: "Priya asked for a review on LRK-118", description: "Payment retries: back off after the third failure", meta: "11:02 AM", severity: "info", unread: true, }, { id: "n2", title: "LRK-104 is blocked", description: "Waiting on the vendor sandbox credentials", meta: "9:15 AM", severity: "attention", unread: true }, { id: "n3", title: "LRK-097 is overdue", description: "Due yesterday, assigned to you", meta: "8:40 AM", severity: "critical", unread: true }, { section: "Yesterday" }, { id: "n4", title: "Onboarding flow shipped", description: "Dana closed the last task in the cycle", meta: "Yesterday", severity: "positive" }, { id: "n5", title: "Weekly digest", description: "14 tasks closed, 3 opened", meta: "Yesterday", icon: "fa-regular fa-envelope" }, { id: "n6", title: "Archived workspace: Sandbox", description: "Nothing has changed here in 90 days", meta: "Aug 29", disabled: true }, ]; /* SuiListEntry[] */ return <SuiList aria-label="Notifications" selection="n2" items={items} onSuiItemPick={(e) => console.log(e.detail)} />; }
<script setup> import { SuiList } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { section: "Today" }, { id: "n1", title: "Priya asked for a review on LRK-118", description: "Payment retries: back off after the third failure", meta: "11:02 AM", severity: "info", unread: true, }, { id: "n2", title: "LRK-104 is blocked", description: "Waiting on the vendor sandbox credentials", meta: "9:15 AM", severity: "attention", unread: true }, { id: "n3", title: "LRK-097 is overdue", description: "Due yesterday, assigned to you", meta: "8:40 AM", severity: "critical", unread: true }, { section: "Yesterday" }, { id: "n4", title: "Onboarding flow shipped", description: "Dana closed the last task in the cycle", meta: "Yesterday", severity: "positive" }, { id: "n5", title: "Weekly digest", description: "14 tasks closed, 3 opened", meta: "Yesterday", icon: "fa-regular fa-envelope" }, { id: "n6", title: "Archived workspace: Sandbox", description: "Nothing has changed here in 90 days", meta: "Aug 29", disabled: true }, ]; // SuiListEntry[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiList aria-label="Notifications" selection="n2" :items="items" @sui-item-pick="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiList } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiList], template: `<sui-list aria-label="Notifications" [(selection)]="selection" [items]="items" (suiItemPick)="onChange($event)"></sui-list>`, }) export class Example { selection?: number | string | undefined; items = [ { section: "Today" }, { id: "n1", title: "Priya asked for a review on LRK-118", description: "Payment retries: back off after the third failure", meta: "11:02 AM", severity: "info", unread: true, }, { id: "n2", title: "LRK-104 is blocked", description: "Waiting on the vendor sandbox credentials", meta: "9:15 AM", severity: "attention", unread: true }, { id: "n3", title: "LRK-097 is overdue", description: "Due yesterday, assigned to you", meta: "8:40 AM", severity: "critical", unread: true }, { section: "Yesterday" }, { id: "n4", title: "Onboarding flow shipped", description: "Dana closed the last task in the cycle", meta: "Yesterday", severity: "positive" }, { id: "n5", title: "Weekly digest", description: "14 tasks closed, 3 opened", meta: "Yesterday", icon: "fa-regular fa-envelope" }, { id: "n6", title: "Archived workspace: Sandbox", description: "Nothing has changed here in 90 days", meta: "Aug 29", disabled: true }, ]; // SuiListEntry[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The list is a role="listbox" of role="option" rows, named by the host aria-label; the selected row reads aria-selected. It paints rows only, so the card is the one frame. A disabled row is skipped by the keyboard and reads aria-disabled.

<sui-card flush style="width:360px;max-width:100%"><sui-list id="list-basic" aria-label="Notifications" selection="n2"></sui-list></sui-card>
// setup(root): properties and events setup(root) { root.querySelector('#list-basic').items = inbox(); }

Discs and avatars

severity draws the status disc on the shared ladder, icon a neutral disc, and avatar or initials a sui-avatar. unread makes the title bold and adds the accent dot, read as "Unread". meta sits on the title line in tabular figures.

<sui-card flush style="width:360px;max-width:100%"><sui-list id="list-discs" aria-label="Members"></sui-list></sui-card>
// setup(root): properties and events setup(root) { root.querySelector('#list-discs').items = members(); }

Avatars with a badge

badge puts a glyph on a disc at the corner of the avatar, the way an inbox marks the kind of event on the actor, and badgeTone paints it from the emphasis roles and the status ladder. It draws only on a row with avatar or initials, through sui-avatar, and is hidden from assistive tech: the title says what happened.

<sui-card flush style="width:360px;max-width:100%"><sui-list id="list-badges" aria-label="Activity"></sui-list></sui-card>
// setup(root): properties and events setup(root) { root.querySelector('#list-badges').items = activity(); }

Density and wrap

Titles truncate on one line by default, so a narrow pane never scrolls sideways. wrap lets them run onto more lines. density is compact, standard or comfortable, the scale the table uses: compact tightens the rows for a triage pane and comfortable gives each row 4px more above and below.

<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start"> <sui-card flush style="width:300px;max-width:100%"><sui-list id="list-compact" density="compact" aria-label="Notifications, compact"></sui-list></sui-card> <sui-card flush style="width:300px;max-width:100%"><sui-list id="list-wrap" wrap aria-label="Notifications, wrapped"></sui-list></sui-card> <sui-card flush style="width:300px;max-width:100%"><sui-list id="list-comfortable" density="comfortable" aria-label="Notifications, comfortable"></sui-list></sui-card> </div>
// setup(root): properties and events setup(root) { const rows = () => [ { id: 'w1', title: longTitle, description: 'Owner: Dana Whitfield', meta: '2d', severity: 'attention', unread: true }, ...inbox().slice(4, 7), ]; root.querySelector('#list-compact').items = rows(); root.querySelector('#list-wrap').items = rows(); root.querySelector('#list-comfortable').items = rows(); }

Loading and empty

loading draws four skeleton rows and marks the host aria-busy. An empty list shows emptyMessage; the default is "Nothing here yet."

<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start"> <sui-card flush style="width:300px;max-width:100%"><sui-list loading aria-label="Notifications, loading"></sui-list></sui-card> <sui-card flush style="width:300px;max-width:100%"><sui-list aria-label="Notifications, empty" empty-message="Nothing new."></sui-list></sui-card> </div>

filter adds a search box that narrows rows by title, description and meta. A miss shows emptyFilterMessage, not emptyMessage: a list of nothing and a list the reader narrowed to none are two states. maxHeight caps the rows and scrolls them.

<sui-card flush style="width:360px;max-width:100%"><sui-list id="list-filter" filter filter-placeholder="Search notifications" max-height="260" aria-label="Notifications" empty-filter-message="No notifications match."></sui-list></sui-card>
// setup(root): properties and events setup(root) { root.querySelector('#list-filter').items = inbox(); }

A row with href is a real link, so middle-click and copy link work and Enter follows it; these three go to other pages of this site. The list then announces itself as a list of links and marks the selected row aria-current="page". suiItemPick still fires on the click first.

<sui-card flush style="width:360px;max-width:100%"><sui-list id="list-links" aria-label="Projects" selection="billing"></sui-list></sui-card>
// setup(root): properties and events setup(root) { root.querySelector('#list-links').items = projects(); }

Navigation without an href

A row with command is a button announced as a link, for a host that navigates in memory. A pick fires suiItemPick and leaves selection alone, so nothing stays selected once the host has moved on; here the pick is logged in place of a route change.

Nothing opened yet.
<sui-card flush style="width:360px;max-width:100%"><div style="display:grid;gap:10px"> <sui-list id="list-command" aria-label="Projects"></sui-list> <div id="list-command-log" style="padding:0 14px 12px;font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing opened yet.</div> </div></sui-card>
// setup(root): properties and events setup(root) { const list = root.querySelector('#list-command'); const log = root.querySelector('#list-command-log'); list.items = projects().map((p) => ({ id: p.id, title: p.title, description: p.description, meta: p.meta, command: true })); list.addEventListener('suiItemPick', (e) => { log.textContent = `Opened ${e.detail.item.title}; selection is ${list.selection === undefined ? 'still unset' : list.selection}.`; }); }

Master and detail

suiItemPick carries { id, item } on every pick, so a detail pane can follow the list; suiSelectionChange fires before it only when the pick changed selection. The selection does not follow the arrow keys: a keyboard user reads the rows first and picks with Enter.

Select a notification
The subject shows here.
Nothing picked yet.
<sui-splitter ratio="0.42" min-ratio="0.3" height="360" aria-label="Inbox"> <div slot="start" style="height:100%;overflow:auto"> <sui-list id="list-master" density="compact" aria-label="Notifications"></sui-list> </div> <div slot="end" style="padding:18px 20px"> <div id="list-detail-title" style="font-size:14.5px;font-weight:700">Select a notification</div> <div id="list-detail-body" style="margin-top:6px;font-size:13px;color:var(--ink-muted);line-height:1.5">The subject shows here.</div> <div id="list-log" style="margin-top:14px;font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing picked yet.</div> </div> </sui-splitter>
// setup(root): properties and events setup(root) { const list = root.querySelector('#list-master'); const title = root.querySelector('#list-detail-title'); const body = root.querySelector('#list-detail-body'); const log = root.querySelector('#list-log'); list.items = inbox(); list.addEventListener('suiItemPick', (e) => { title.textContent = e.detail.item.title; body.textContent = e.detail.item.description || ''; log.textContent = `suiItemPick: ${e.detail.id}.`; }); }

Keyboard

KeyAction
TabThe list is one tab stop: the row that last held focus, else the selected row, else the first enabled row.
ArrowDown ArrowUpMoves to the next or previous enabled row without wrapping. Focus moves; the selection does not.
Home EndMoves to the first or last enabled row.
Enter SpacePicks the focused row: writes selection, which fires suiSelectionChange when it changed, then emits suiItemPick. On a linked row the browser then follows the href.
Any letterMoves to the next row whose title starts with the letters typed.
ArrowDown in the search boxLeaves the search box for the tab stop row.

API

<sui-list>

Properties

PropertyAttributeTypeDefaultDescription
densitydensity"comfortable" | "compact" | "standard"'standard'Row height, on the scale the table and the grid use: standard, the default, for a reading list, compact for a dense triage pane, comfortable for a roomier list with 4px more padding above and below each row.
emptyFilterMessageempty-filter-messagestringstring | undefinedText shown when the search box narrowed the rows to none. An empty list and a search that found nothing are two states, so this is a second prop.
emptyMessageempty-messagestringstring | undefinedText shown when items holds no rows. The empty slot replaces it.
filterfilterbooleanfalseSearch box above the rows. It narrows them to the ones whose title, description or meta contains the text, case-insensitive, and leaves selection alone. Reflected.
filterPlaceholderfilter-placeholderstringstring | undefinedPlaceholder of the search box.
itemRenderproperty only((item: SuiListItem) => SuiRenderResult) | undefinedDraws a row's body yourself; return a string or an element. The row keeps its selection, focus and keyboard, and the disc, dot and text stack are not drawn. A callback that throws costs only its row, which draws the built-in body, and the error is logged once. Property, not attribute.
itemsproperty onlySuiListEntry[][]Rows and section headings: { id, title, description?, meta?, icon?, avatar?, initials?, badge?, badgeTone?, unread?, severity?, disabled?, href?, target?, command? } or { section }. Property, not attribute. Put an aria-label on the host to name the list.
loadingloadingbooleanfalseDraws four skeleton rows in place of the items and marks the list busy.
maxHeightmax-heightnumbernumber | undefinedCaps the height of the rows in px; beyond it they scroll vertically inside the list. Unset, the list is as tall as its rows and the container scrolls.
selectionselectionSuiKeynumber | string | undefinedId of the selected row, as given (a number stays a number). Mutable: a pick writes it and fires suiSelectionChange when it changed, then suiItemPick; write it to select from outside, or clear it with undefined. Property, not attribute.
wrapwrapbooleanfalseLets long titles and descriptions wrap onto more lines. Off, they truncate with an ellipsis on one line, the way a mail client keeps every row the same height. Either way the list never scrolls sideways.

Events

EventDetailReactVueDescription
suiItemPickSuiListItemPickDetailonSuiItemPick@sui-item-pickFires when a row is picked by click, Enter or Space; detail is { id, item }. It fires on every pick, so picking the selected row again reports again. On a linked row it fires on the click, before the browser follows the href; on a command row it fires once and writes nothing, since the host is about to navigate.
suiSelectionChangenumber | string | undefinedonSuiSelectionChange@sui-selection-changeFires when a pick changes selection, before suiItemPick; detail is the new selection. A command row and a second pick of the selected row leave it unchanged and do not fire it. Does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the tab stop row: the last focused row, else the selected one, else the first.

Slots

SlotDescription
emptyWhat to show in place of the empty sentence when items holds no rows: a line and a button offering the next step. The same slot name the grid, the tree and the tree table take.

Types

SuiAvatarBadgeTone typeColor role of the corner badge: the two emphasis roles plus the shared status ladder.
No members.
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiListEntry typeOne entry of items: a row or a section heading.
No members.
SuiListItem interfaceOne row of the list.
idSuiKeyStable identity: the value selection holds and suiItemPick reports, kept as given.
titlestringOne line, sentence case. Truncates unless the list sets wrap.
description?stringSupporting line under the title.
meta?stringTrailing text on the title line in tabular figures: a time, a count.
icon?stringFont Awesome class drawn in a neutral disc before the text.
avatar?stringImage URL for an avatar disc before the text; wins over icon.
initials?stringLetters for an avatar disc without an image.
badge?stringFont Awesome class for a small glyph on a disc at the avatar's corner, the way an inbox marks the kind of event on the actor's avatar: a comment, an assignment, a mention. Drawn only on a row with avatar or initials, through sui-avatar's own badge, and hidden from assistive tech; the title carries the meaning.
badgeTone?SuiAvatarBadgeToneColor role of the badge disc: brand, accent, or the status ladder positive, info, attention, critical and neutral (the default), as sui-avatar paints them.
unread?booleanBold title and the accent dot, read as "Unread".
severity?SuiSeverityStatus disc before the text, on the shared ladder; wins over icon.
disabled?booleanInert row: no focus, no selection.
href?stringMakes the row a real link. A list with any linked row announces itself as a list of links and marks the selected row with aria-current.
target?stringLink target for a row with href. _blank gets rel="noopener".
command?booleanMakes the row a navigation control without an href: a button announced as a link, for a host that navigates in memory. A pick fires suiItemPick and leaves selection alone, so nothing stays selected after the host has moved on; selection written by the host still marks the row aria-current. The list counts it as a linked row. Ignored beside href.
SuiListItemPickDetail interfaceDetail of suiItemPick.
idSuiKey
itemSuiListItem
SuiListSection interfaceA heading row over the rows that follow it, until the next heading.
sectionstring
SuiRenderResult type
No members.
SuiSeverity type
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

Color28

--accentthe one chart highlight: the flagged series, down candles, the peak cell--bordercard + table outer borders--brand--brand-soft-ink--control-borderthe resting edge of every field and control: 3:1 on its grounds--danger--danger-soft--disabled-bg--disabled-ink--focus-halo--focus-ring--hairlinerow separators--ink--ink-faintmetadata beside body ink: 4.8:1 on the page grounds (5.4:1 in dark)--ink-mutedsecondary text: 7:1 on the page grounds and 1.5 times faint--ink-muted-on-washthe muted ink a selected, current or pressed row paints: 4.5:1 on that wash--ink-placeholdera field's placeholder, not a text tier: 4.5:1 on the page grounds--status-attention-bg--status-attention-inkOpen, Pending, In transit--status-neutral-bg--status-neutral-ink--status-positive-bg--status-positive-inkPaid, Cleared, Active--surfacecards, tables, inputs--surface-hovera hovered row or quiet control on any ground: the ink, translucent--surface-raisedmenus, popovers, toasts: the card in light, a step above it in dark--surface-selecteda selected row or option: the primary's hue, always heavier than a hover--surface-sunkentable headers, wells

Typography1

Ag--font-sans

Spacing and size5

42--control-lgform-page inputs34--control-mdDEFAULT toolbar/filter controls12--control-pad-x28--control-sm700--z-dropdown

Radius1

--radius-mdALL controls: inputs, buttons, selects

Elevation1

--shadow-popdropdowns, popovers, toasts

Motion3

--duration-fasthovers, presses, every exit--duration-overlayevery entrance: palette, popover, dialog, drawer, toast, menu, select list--ease-out

Every name above is listed with its value and its role in Foundations.

See also