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.
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.
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.
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.
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.
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.
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.
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.
The list is one tab stop: the row that last held focus, else the selected row, else the first enabled row.
ArrowDownArrowUp
Moves to the next or previous enabled row without wrapping. Focus moves; the selection does not.
HomeEnd
Moves to the first or last enabled row.
EnterSpace
Picks 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 letter
Moves to the next row whose title starts with the letters typed.
ArrowDown in the search box
Leaves the search box for the tab stop row.
API
<sui-list>
Properties
Property
Attribute
Type
Default
Description
density
density
"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.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Text 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.
emptyMessage
empty-message
stringstring | undefined
Text shown when items holds no rows. The empty slot replaces it.
filter
filter
boolean
false
Search 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.
Draws 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.
items
property only
SuiListEntry[]
[]
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.
loading
loading
boolean
false
Draws four skeleton rows in place of the items and marks the list busy.
maxHeight
max-height
numbernumber | undefined
Caps 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.
selection
selection
SuiKeynumber | string | undefined
Id 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.
wrap
wrap
boolean
false
Lets 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
Event
Detail
React
Vue
Description
suiItemPick
SuiListItemPickDetail
onSuiItemPick
@sui-item-pick
Fires 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.
suiSelectionChange
number | string | undefined
onSuiSelectionChange
@sui-selection-change
Fires 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
Method
Signature
Description
setFocus()
setFocus() => Promise<void>
Moves focus to the tab stop row: the last focused row, else the selected one, else the first.
Slots
Slot
Description
empty
What 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
SuiAvatarBadgeTonetypeColor role of the corner badge: the two emphasis roles plus the shared status ladder.
No members.
SuiKeytypeA 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.
SuiListEntrytypeOne entry of items: a row or a section heading.
No members.
SuiListIteminterfaceOne row of the list.
id
SuiKey
Stable identity: the value selection holds and suiItemPick reports, kept as given.
title
string
One line, sentence case. Truncates unless the list sets wrap.
description?
string
Supporting line under the title.
meta?
string
Trailing text on the title line in tabular figures: a time, a count.
icon?
string
Font Awesome class drawn in a neutral disc before the text.
avatar?
string
Image URL for an avatar disc before the text; wins over icon.
initials?
string
Letters for an avatar disc without an image.
badge?
string
Font 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?
SuiAvatarBadgeTone
Color 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?
boolean
Bold title and the accent dot, read as "Unread".
severity?
SuiSeverity
Status disc before the text, on the shared ladder; wins over icon.
disabled?
boolean
Inert row: no focus, no selection.
href?
string
Makes 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?
string
Link target for a row with href. _blank gets rel="noopener".
command?
boolean
Makes 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.
SuiListItemPickDetailinterfaceDetail of suiItemPick.
id
SuiKey
item
SuiListItem
SuiListSectioninterfaceA heading row over the rows that follow it, until the next heading.
section
string
SuiRenderResulttype
No members.
SuiSeveritytype
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.