Skip to content

List item

In review
Live · web

Sessions: tab / click a row to select (one cyan indicator)

Settings: value rows, a disabled row, and a trailing control (toggle = its own target)

Push notificationsAlerts when a session needs you

Messages: tap a row to expand (chevron rotates down, then up); tap again to collapse

Accordion variant shipped. The trailing chevron now rotates down-to-up and the body reveals via an interpolated grid-row track, not a display swap. Selection moved to the leading slot.

Selected session: s2 · push notifications on · open message: m1. The island is live.

Collapsible / accordion variant

Pass expandable (with expanded + onToggle + a details body) to turn a row into a collapsible accordion, the pattern the native Android messages list uses: tap a row to reveal its message body, tap again to collapse. Key behaviours:

  • Affordance: the trailing chevron points down at rest and rotates up when expanded (the canonical tree disclosure, made interpolatable), not the right-pointing chevron, which stays reserved for genuinely navigational rows.
  • Reveal: the body is a sibling role="region" below the row, revealed via an interpolated grid-template-rows: 0fr → 1fr + opacity, never a display or fill swap; prefers-reduced-motion drops the transition.
  • A11y: the row stays one <button> carrying aria-expanded + aria-controls; the region is aria-labelledby the primary text and hidden while collapsed.
  • Selection vs expansion: selection now renders its cyan check in the leading slot (a cyan edge bounding the run, cyan ink, and a leading mark), so selection and the trailing nav/expand chevron no longer compete for the one trailing slot.
// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;
// this is the canonical API the published @dizyx/nockerl-react package exposes.)
import { NockerlListItem } from '@dizyx/nockerl-react';
export function SessionList({ sessions, activeId, onSelect }: SessionListProps) {
return (
<div className="nk-card" role="listbox" aria-label="Sessions">
{sessions.map((s) => (
<NockerlListItem
key={s.id}
primary={s.name}
secondary={s.statusLine}
status={s.status} // 'success' | 'warning' | 'error' | 'info', never cyan
value={s.timeAgo}
chevron
selected={s.id === activeId}
onSelect={() => onSelect(s.id)}
/>
))}
</div>
);
}
PropTypeDefaultDescription
primary *stringPrimary text. Carries the row's accessible name (label.large role).
secondarystringOptional supporting line under the primary text (body.small role).
statusNockerlListItemStatusOptional leading status mark: renders the canonical status ICON (shape + color dual-coding, never a bare dot) in the status color. Status colors only, never cyan.
leadingIconReact.ReactNodeOptional leading ICON (any node) for action rows (share / rename / delete). Shown in the leading slot when the row is neither selected nor carrying a status mark.
dangerbooleanfalseDestructive treatment. The primary text + leading icon go status-error (a delete row).
valuestringOptional trailing value text (e.g. a count, a timestamp).
trailingReact.ReactNodeOptional trailing NODE (a NockerlBadge / NockerlChip / status pill) shown before the chevron. For rich rows: an accordion header's tone pill, a tool-card's state chip. Compose a real component (NockerlBadge / NockerlStatusDisc / NockerlChip), never a hand-rolled facsimile.
chevronbooleanfalseShow a trailing NAV chevron (right-pointing, for genuinely navigational rows).
expandablebooleanfalseMake the row an accordion: trailing chevron points DOWN at rest, rotates UP when expanded; the row gets aria-expanded + aria-controls and reveals a body region below it. Mutually exclusive with the nav chevron.
expandedbooleanfalseExpanded state for an expandable row; drives the chevron rotation + body reveal.
onToggle() => voidToggle handler for an expandable row (whole-row tap). Ignored while disabled.
detailsReact.ReactNodeThe body of an expandable row, revealed when the row expands.
selectedbooleanfalseSelected state: a cyan edge bounding the run, cyan ink, and a LEADING check. NOT a glow, not a left-rail.
disabledbooleanfalseInert + clearly-seen (never invisible) state.
onSelect() => voidActivate handler (whole-row tap). Ignored while disabled.

Also accepts the native <button> attributes (e.g. onClick, disabled, id, aria-*, data-*), forwarded straight through, plus a forwarded ref.

Cross-platform drift (tracked for reconciliation)

The grammar is shared; these are the real divergences between the shipped platforms, tracked in the Review queue:

  • No Swift component: Voice builds rows inline in each view (HistoryView.row(_:)); there is no NockerlListItemRow equivalent. Status, spacing, and the leading mark are re-implemented per row, exactly the drift the Compose grammar exists to prevent. Extracting a shared Swift row is the open conformance item.
  • Leading mark shape (resolved): the leading mark is an icon (shape + color dual-coding), never a bare colored dot. Compose ships the icon at 18dp; web now renders it inside the NockerlListItem primitive (pass status, get the icon by default; no wrapper needed). Voice (a colored dot / capsule badge today) is the remaining conformance item.
  • Selection model: the one selection expression is a cyan edge bounding the run, cyan ink and a check, never a left stripe and never a wash. An earlier draft used a soft cyan wash; design law 6 (state by outline, not fill) superseded that, and the web row now carries the outline. The web row exposes an explicit selected; the Compose scaffold (a caller modifier background) still fills, so Android is the platform that lags here, and Voice has no persistent selection at all (it toggles expanded).
  • Trailing control: web makes the trailing toggle a separate role="switch"; Voice uses NockerlIconButtons; Compose leaves it to the row’s trailing slot.