Skip to main content

list — a collection of items, each its own shape

UiList renders a <ul> of items. Each item is a small record in its own right: a label (plain or a whole node), a leading icon, a muted description, per-item actions, and — the reason UiList exists — any UiNode as content. A list item can hold a form, a table, a chart or a nested stack.

Reach for UiList when the items are not a neat grid: activity feeds, search results, agent tool-call cards, anything where the rows differ from each other or carry rich bodies. When every row has the same columns and you want sorting, selection and stack-on-mobile, use table instead.

Live — items with an icon plus description and a per-item action, one item whose header is a rich labelNode, one collapsible item whose body is a whole stack, and a pagination footer.

Fields​

FieldTypeMeaning
idStringNode id and DOM id of the wrapping <div class="sui-list">.
titleStringRendered as the <h2> in the list header. Omitted when absent.
iconStringLeading icon token in the header, before the title — lets a page's main list carry the same icon as its navigation entry. See icons.
headerExtraUiNodeNode rendered in the header row between title and actions — built for compact inline forms like a search field (the form chrome is stripped).
itemsList<UiList.Item>The rows. Defaults to an empty list. See below.
actionsList<UiAction>List-level buttons in the header, next to the title (Refresh, Add, …). Defaults to an empty list.
actionsOverflowWRAP · MENUWhat the button bar does when its buttons do not fit one row. WRAP (default) takes another line; MENU keeps one row and folds the buttons that do not fit, from the end, into a trailing "⋯" menu — see responsive.
paginationUiList.PaginationOptional pager rendered under the items. null = no pager.
cssClassStringExtra CSS class added next to sui-list.

Item fields​

UiList.Item is a UiNode of type "item", so it has everything a node has (cssClass, display, the event triggers) and, above all, an address: a patch can REMOVE it, REPLACE it with another item or MERGE a field of it, and the tree copy behind bus.snapshot() follows. It is only ever drawn by the list it sits in; JSON that still writes items without a type is read as items.

FieldTypeMeaning
idStringItem id. Becomes the <li> id and its data-id, so a single item can be patched on its own.
labelStringThe item headline as plain text. Kept as the accessible fallback even when labelNode is set.
labelNodeUiNodeRich header: rendered instead of the plain label text. Lets a row title carry structure — a name plus a status badge, an icon plus a menu-button.
iconStringLeading icon token before the plain label. Ignored when labelNode is set — a rich header owns its own layout. See icons.
descriptionStringMuted second line. Only rendered when content is absent — content takes the same slot.
onClickUiTriggerFired when the item's label is clicked. null = a static, non-clickable label.
actionsList<UiAction>Per-item buttons, rendered in their own trailing cell. Defaults to an empty list.
contentUiNodeAny node, rendered as the item's body. Wins over description.
collapseSummaryStringWhen set, the whole item body is wrapped in a <details> disclosure with this text as the <summary>.
collapseOpenbooleanDefaults to false. true renders the disclosure open. Ignored when collapseClientControlled is set.
collapseSummaryIdStringShorthand for a collapseSummaryNode that is a text with this id and collapseSummary as its words. The summary is then a node a patch can name: REPLACE it with another text, or MERGE {"text": …} on it (flip "running" to "done") without touching the body.
collapseSummaryNodeUiNodeThe summary as a node of its own, rendered inside the <summary> instead of collapseSummary — a text with an id, or a stack with an icon and a badge. collapsible(UiNode, boolean) sets it.
collapseClientControlledbooleanDefaults to false. true renders collapsed and tags the element data-sui-client-collapse, so the user's manual expand/collapse survives re-renders and streaming patches.

Pagination​

UiList.Pagination is informational unless you give it a trigger template.

FieldTypeMeaning
pageintCurrent page, 1-based.
sizeintItems per page. The renderer derives the page count as ceil(total / size).
totallongTotal number of items across all pages.
pageTriggerUiTriggerTemplate fired when a page button is clicked. The renderer substitutes the literal {page} in the trigger's url with the target page number. null renders both buttons disabled — the pager becomes a read-out.

Building one​

UiList.of("activity", "Recent activity")
.action(UiAction.secondary("l-refresh", "Refresh")
.onClick(UiTrigger.api("POST", "/activity/refresh")))

// icon + description + a per-item action
.item(UiList.Item.of("l1", "Order #1024 shipped")
.icon("success")
.description("web-frontend · 2 minutes ago")
.onClick(UiTrigger.go("/orders/1024"))
.action(UiAction.secondary("l1-show", "Show").icon("show")
.appearance(UiAction.Appearance.ICON)
.onClick(UiTrigger.go("/orders/1024"))))

// a rich header instead of a plain label
.item(UiList.Item.of("l3", "Import job")
.labelNode(UiStack.of(
UiIcon.of("l3-icon", "document"),
UiText.of("l3-title", "Import job"),
UiText.of("l3-badge", "· running"))
.direction(UiStack.Direction.HORIZONTAL).gap(8))
.description("3 of 8 files processed"))

// a collapsible item whose body is a whole node
.item(UiList.Item.of("l4", "Nightly build")
.collapsible("Nightly build — 3 warnings (click to expand)", false)
.content(UiText.of("l4-1", "Build passed with 3 lint warnings.")))

// a live card whose open state belongs to the user, not the server
.item(UiList.Item.of("l5", "Tool call")
.collapsibleClient("search_web — running…", "l5-sum")
.content(UiText.of("l5-body", "")))

.paginate(1, 4, 42, UiTrigger.api("GET", "/activity?page={page}"));

Notes​

list or table? If every row has the same fields and the user wants to compare them column by column, it is a table — that is where sorting, filtering, row selection and stackOnMobile live. If the rows differ, or any row needs a body richer than a string, it is a UiList. Items are also individually addressable by id, which makes a list the better fit for feeds that grow by APPEND.

content replaces description, labelNode replaces icon. These are the two overrides worth memorising: the renderer picks content over description for the body slot, and labelNode over icon + label for the header slot. Setting both members of a pair is not an error — the loser is simply never drawn, though label is still kept as the plain-text fallback.

Client-controlled collapse is for live content. A server-driven open attribute fights the user every time a patch arrives: they collapse a card, the next re-render springs it open again. collapsibleClient(...) hands the state to the browser and tags the element so the morpher leaves it alone — which is exactly what streaming tool-call and sub-agent cards need. Pair it with collapseSummaryId so the summary can flip from "running…" to "done" via a targeted REPLACE while the body keeps streaming.

Pagination without a pageTrigger is decoration. The renderer emits both page buttons disabled when no template is given, which is a fine read-out but catches people out. Pass the trigger with a literal {page} in the URL and the renderer substitutes it per button — no per-page node building on the server.

Patch the item, not the list. Each <li> carries the item id, so a status change is one patch on that item — not a re-render of the whole collection. That keeps scroll position, focus and every other item's disclosure state intact. What a patch can name inside a list, and what it may carry:

TargetREMOVEREPLACE withMERGE
an itemtakes the row outanother item (Operation.replace(id, item))any of its fields — label, description, content, …
the summary (collapseSummaryId)—a UiText{"text": …}
a header or item actiontakes the button outanother actionlabel, enabled, confirm, loading, …

See patches.

See also​

  • table — same data, grid semantics.
  • detail — a single record instead of a collection.
  • action — list-level and per-item buttons.
  • text — the usual content of a simple item.
  • Triggers cookbook — item clicks, paging, streaming into an item.