registry:ui · Layout
list-item — the row, as a system
The list row as a system (F7 coverage, shadcn composition): AUTO-VARIANT chrome — standalone rows carry their own terminal surface, ItemGroup owns the frame (native section+ul, default/muted/plain modes, optional label, divider policy: 38% auto hairlines structurally exclusive with full-strength ItemDivider). The ItemEnd lane hosts after/actions/chevron; the grid presence matrix stays four bits (:has()-driven, no implicit tracks). ItemField wires settings rows (label[for], describedby chains, icon glyph inline-start of the label) and seven thin adapters — ItemToggle / ItemCheckbox / ItemRadio / ItemSelect / ItemInput / ItemSegmented / ItemStepper — compose the existing controls without reimplementing them (segmented rides native radio ToggleGroup semantics, stepper rides NumberInput with typing first-class). layout=media rides custom properties; narrow groups wrap the end lane at 30rem via container query.
list-item
The base composition: ItemContent (title + description) and the ItemEnd lane on an outline Item. Every other slot is optional — the grid template rewrites itself around what exists.
<script lang="ts">
import {
Item,
ItemContent,
ItemTitle,
ItemDescription,
ItemEnd,
} from '@ui/list-item/index';
import IconButton from '@ui/icon-button/icon-button.svelte';
import Icon from '@ui/icon.svelte';
</script>
<!-- ItemContent is the required slot; media/end/header/footer are
presence-optional — their existence rewrites the grid template.
The row IS the raised surface: trailing press controls ride FLAT
and frameless (IconButton's ghost default) — the frame is the
row's, not each glyph's. -->
<Item variant="outline">
<ItemContent>
<ItemTitle>Deploy #482</ItemTitle>
<ItemDescription>main · 4f2a1c · 2 minutes ago</ItemDescription>
</ItemContent>
<ItemEnd>
<IconButton iconOnly text="Rerun deploy" class="size-7!">
{#snippet icon()}<Icon name="check" />{/snippet}
</IconButton>
<IconButton iconOnly text="More actions" class="size-7!">
{#snippet icon()}<Icon name="ellipsis" />{/snippet}
</IconButton>
</ItemEnd>
</Item>proof
Standalone ladder
variant=auto (the default) resolves FROM CONTEXT: outside any group the row carries its own surface — 1px border + terminal-muted fill + the 2xs hard shadow + bevel. Explicit variants stay escape hatches: default paints nothing, outline frames, muted fills. data-item-chrome on the root always tells you what resolved — inspect it.
list-item · standalone ladder
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
</script>
<div class="grid w-full gap-6 md:grid-cols-2">
<div class="flex flex-col gap-2">
<span class="text-muted-foreground text-[11px] uppercase tracking-[0.14em]">auto · surface</span>
<Item>
<ItemContent>
<ItemTitle>the auto row</ItemTitle>
<ItemDescription>data-item-chrome="surface"</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>own frame</ItemAfter></ItemEnd>
</Item>
{#each ['default', 'outline', 'muted'] as const as v (v)}
<Item variant={v}>
<ItemContent>
<ItemTitle>variant="{v}"</ItemTitle>
<ItemDescription>explicit — always wins over context</ItemDescription>
</ItemContent>
</Item>
{/each}
</div>
<div class="flex flex-col gap-2">
<span class="text-muted-foreground text-[11px] uppercase tracking-[0.14em]">density</span>
{#each ['default', 'sm', 'xs'] as const as s (s)}
<Item density={s}>
<ItemContent>
<ItemTitle>size="{s}"</ItemTitle>
<ItemDescription>padding, gaps and type scale — never geometry</ItemDescription>
</ItemContent>
</Item>
{/each}
</div>
</div>proof
Group modes
The group owns the surface: mode=default frames the list (1px border + xs shadow) with auto hairlines between adjacent rows (38% mix — a long stack never becomes a wall of full-black bars); mode=muted is one terminal-muted slab (dividers forced off); mode=plain paints nothing — the host surface owns it — and opts into hairlines explicitly. inset adds fixed 0.75rem inline margins. label renders a real section + aria-labelledby. Inside, auto rows yield their chrome — explicit variant rows remain the documented escape hatch, not the default.
list-item · group modes
- press-button default mode · labeled section · auto hairlines
- with an explicit divider full-strength — one source per edge, structurally exclusive12:04
- code-card registry:ui · highlight + copy workbench
- muted slab terminal-muted fill · no frame · no dividers, ever
- rows read as one surface2
- plain group nothing painted — this page is the surface; hairlines opted in
- for menus, panels, terminals
- inset group fixed 0.75rem inline margins — boolean, no responsive enum
- same paint as defaultfixed geometry
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
import Icon from '@ui/icon';
</script>
<ItemGroup label="registry" ruler="media-content-end" class="max-w-lg">
<Item href="#group-modes">
<ItemMedia variant="icon"><Icon name="folder" /></ItemMedia>
<ItemContent>
<ItemTitle>press-button</ItemTitle>
<ItemDescription>default mode · labeled section · auto hairlines</ItemDescription>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
<ItemDivider />
<Item>
<ItemContent>
<ItemTitle>with an explicit divider</ItemTitle>
<ItemDescription>full-strength — one source per edge, structurally exclusive</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>12:04</ItemAfter></ItemEnd>
</Item>
<Item href="#group-modes">
<ItemContent>
<ItemTitle>code-card</ItemTitle>
<ItemDescription>registry:ui · highlight + copy workbench</ItemDescription>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
</ItemGroup>
<ItemGroup mode="muted" label="strong relations" class="max-w-lg">
<Item>
<ItemContent>
<ItemTitle>muted slab</ItemTitle>
<ItemDescription>terminal-muted fill · no frame · no dividers, ever</ItemDescription>
</ItemContent>
</Item>
<Item>
<ItemContent>
<ItemTitle>rows read as one surface</ItemTitle>
</ItemContent>
<ItemEnd><ItemAfter>2</ItemAfter></ItemEnd>
</Item>
</ItemGroup>
<ItemGroup mode="plain" dividers="auto" label="host-owned (plain)" class="max-w-lg">
<Item>
<ItemContent>
<ItemTitle>plain group</ItemTitle>
<ItemDescription>nothing painted — this page is the surface; hairlines opted in</ItemDescription>
</ItemContent>
</Item>
<Item>
<ItemContent>
<ItemTitle>for menus, panels, terminals</ItemTitle>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
</ItemGroup>
<ItemGroup mode="default" inset label="inset" class="max-w-lg">
<Item>
<ItemContent>
<ItemTitle>inset group</ItemTitle>
<ItemDescription>fixed 0.75rem inline margins — boolean, no responsive enum</ItemDescription>
</ItemContent>
</Item>
<Item>
<ItemContent>
<ItemTitle>same paint as default</ItemTitle>
</ItemContent>
<ItemEnd><ItemAfter>fixed geometry</ItemAfter></ItemEnd>
</Item>
</ItemGroup>proof
Slot topology
Four top-level presence bits — media · end · header · footer — rewrite the template; everything trailing lives INSIDE ItemEnd: ItemAfter (non-interactive metadata) flows before your controls (raw in the lane — flat physics by ambient — or one ItemActions cluster: the ButtonGroup-backed action lane, DialogFooter semantics) before ItemChevron (the decorative glyph). ItemMedia is icon / avatar-host / image (src+alt render the img). Header and footer are optional full rows. Link rows never contain interactive descendants — the last row shows where actions belong.
list-item · slot topology
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
import Icon from '@ui/icon';
import Avatar from '@ui/avatar.svelte';
import IconButton from '@ui/icon-button/icon-button.svelte';
</script>
<Item variant="outline">
<ItemMedia variant="icon"><Icon name="folder" /></ItemMedia>
<ItemContent>
<ItemTitle>registry folder</ItemTitle>
<ItemDescription>12 components · updated today</ItemDescription>
</ItemContent>
<ItemEnd>
<ItemAfter tone="default">3 open</ItemAfter>
<span class="text-[10px] opacity-60">edit</span>
</ItemEnd>
</Item>
<Item variant="outline">
<ItemMedia>
<Avatar name="Ada Lovelace" tooltip={false} />
</ItemMedia>
<ItemContent>
<ItemTitle>Ada Lovelace</ItemTitle>
<ItemDescription>maintainer · 3 commits today</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>12:04</ItemAfter></ItemEnd>
</Item>
<Item variant="outline">
<ItemMedia variant="image" src="/blueprints/press-button.svg" alt="press-button blueprint" />
<ItemContent>
<ItemTitle>press-button</ItemTitle>
<ItemDescription>the physical press law · blueprint</ItemDescription>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
<Item variant="outline">
<ItemHeader>
<span class="text-muted-foreground text-[11px] uppercase tracking-[0.14em]">
pull request #2182
</span>
<span class="pill">open</span>
</ItemHeader>
<ItemContent>
<ItemTitle>feat: the presence matrix</ItemTitle>
<ItemDescription>@gaubee wants to merge 3 commits into main</ItemDescription>
</ItemContent>
<ItemEnd>
<IconButton iconOnly text="Close pull request" class="size-7!">
{#snippet icon()}<Icon name="x" />{/snippet}
</IconButton>
</ItemEnd>
<ItemFooter>
<span class="text-muted-foreground text-[11px]">3 checks passed · 2 files changed</span>
<span class="text-muted-foreground text-[11px]">updated 14 minutes ago</span>
</ItemFooter>
</Item>proof
Media layout & the narrow law
layout=media (inherited from the group) switches custom properties only — a larger media square (3rem), wider gutters, top-aligned media; never a new presence bit. The narrow law: the group's ul IS the container; at ≤30rem the end lane drops to its own full row — a container query, not a viewport breakpoint. ItemEnd wrap=never keeps the lane on the main row.
list-item · media & narrow law
- Gmedia layout rows media self-starts, content gap loosens — the custom-property switchcompiler
- Aworks for feeds and threads the same four presence bits, a calmer rhythmdynabook
- separator registry:ui · the hr, W3C-first
- wrap=never row the lane refuses to split12:04:33
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
import Icon from '@ui/icon';
import Avatar from '@ui/avatar.svelte';
</script>
<ItemGroup layout="media" ruler="media-content-end" mode="plain" dividers="auto" class="max-w-lg">
<Item>
<ItemMedia>
<Avatar name="Grace Hopper" size="sm" tooltip={false} />
</ItemMedia>
<ItemContent>
<ItemTitle>media layout rows</ItemTitle>
<ItemDescription>media self-starts, content gap loosens — the custom-property switch</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>compiler</ItemAfter></ItemEnd>
</Item>
<Item>
<ItemMedia>
<Avatar name="Alan Kay" size="sm" tooltip={false} />
</ItemMedia>
<ItemContent>
<ItemTitle>works for feeds and threads</ItemTitle>
<ItemDescription>the same four presence bits, a calmer rhythm</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>dynabook</ItemAfter></ItemEnd>
</Item>
</ItemGroup>
<!-- narrow law: the group's ul IS the container; at ≤30rem the end lane
takes its own row — resize the pane to watch it fold -->
<div class="max-w-[19rem]">
<ItemGroup mode="plain" dividers="auto" ruler="media-content-end">
<Item href="#media-narrow">
<ItemMedia variant="icon"><Icon name="fileText" /></ItemMedia>
<ItemContent>
<ItemTitle>separator</ItemTitle>
<ItemDescription>registry:ui · the hr, W3C-first</ItemDescription>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
<Item>
<ItemContent>
<ItemTitle>wrap=never row</ItemTitle>
<ItemDescription>the lane refuses to split</ItemDescription>
</ItemContent>
<ItemEnd wrap="never"><ItemAfter>12:04:33</ItemAfter></ItemEnd>
</Item>
</ItemGroup>
</div>proof
The density ladder
Four densities, every number an equation from the 4px ruler: text 11/12/13/15px on lines 16/18/20/24px, rows 28/32/40/48px, media = one line (icon) or two (image), the outer inset ALWAYS equals the media seam (B=G). Density resolves from context: the group provides, rows inherit, an explicit size overrides — and the whole cascade is one data-density stamp plus inherited --jx-* variables, zero per-size branches in component css.
list-item · density ladder
- the lg row · 15px text · 24px line · 48pxhit 44px
- the default row · 13px · 20px · 40pxhit 44px
- the sm row · 12px · 18px · 32pxhit 44px
- the xs row · 11px · 16px · 28pxhit 44px
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
</script>
{#each [['lg', 'the lg row · 15px text · 24px line · 48px'], ['default', 'the default row · 13px · 20px · 40px'], ['sm', 'the sm row · 12px · 18px · 32px'], ['xs', 'the xs row · 11px · 16px · 28px']] as const as [d, note] (d)}
<div>
<span class="text-muted-foreground mb-1 block text-[11px] uppercase tracking-[0.14em]">{d}</span>
<ItemGroup mode="plain" dividers="auto" density={d}>
<Item>
<ItemContent>
<ItemTitle>{note}</ItemTitle>
</ItemContent>
<ItemEnd><ItemAfter>hit 44px</ItemAfter></ItemEnd>
</Item>
</ItemGroup>
</div>
{/each}proof
Settings section (adapters)
The seven thin adapters are the settings page as one-liners: ItemToggle, ItemCheckbox, ItemRadio, ItemSelect, ItemInput, ItemSegmented, ItemStepper. Each is ItemField + the existing control — the control keeps every native behavior (keyboard, form participation, bindable state); the adapter only wires ids and suppresses the control's duplicate label plumbing. ItemSegmented rides native radio semantics (arrow-walk, one tab stop, no re-press clear) and resolves the borderless bare rung inside the integrated row; ItemStepper rides NumberInput — typing first-class, hold acceleration, min/max clamp. Every adapter (and ItemField itself) takes an icon snippet: a decorative glyph inline-start of the label, aria-hidden, bring-your-own. label[for] makes click-row-to-activate free; description and error chain into aria-describedby; error flips aria-invalid. The live state below is bound — flip anything.
list-item · settings adapters
- skip typechecking during watch
- requires the beta flag
- tagged releases
- url/tel/email/search carry their glyph by default — the trailing edge inside a list-item lane
- text carries the Type glyph by default — like every text-like type; icon= opts out
- Preview native radio semantics — arrow-walk, one tab stop
bound: fast on · telemetry off · default · "ops@jixoai.dev" · "" · balanced · 4
<script lang="ts">
import {
ItemGroup,
ItemToggle,
ItemCheckbox,
ItemRadio,
ItemSelect,
ItemInput,
ItemSegmented,
ItemStepper,
} from '@ui/list-item/index';
import Icon from '@ui/icon';
let autoplay = $state(true);
let telemetry = $state(false);
let density = $state('default');
let alertEmail = $state('ops@jixoai.dev');
let projectName = $state('');
let previewMode = $state('balanced');
let workers = $state(4);
</script>
{#snippet cpuGlyph()}<Icon name="braces" />{/snippet}
<ItemGroup label="workspace">
<ItemToggle
label="Fast builds"
description="skip typechecking during watch"
bind:checked={autoplay}
/>
<ItemCheckbox label="Telemetry" error="requires the beta flag" bind:checked={telemetry} />
<ItemRadio name="channel" value="stable" label="Stable channel" description="tagged releases" checked />
<ItemRadio name="channel" value="canary" label="Canary channel" disabled />
<ItemSelect label="Density" bind:value={density}>
<option value="default">default</option>
<option value="sm">sm</option>
<option value="xs">xs</option>
</ItemSelect>
<ItemInput type="email" label="Alert email" description="url/tel/email/search carry their glyph by default — the trailing edge inside a list-item lane" bind:value={alertEmail} />
<ItemInput label="Project name" description="text carries the Type glyph by default — like every text-like type; icon={null} opts out" bind:value={projectName} />
<ItemSegmented
label="Preview"
description="native radio semantics — arrow-walk, one tab stop"
options={[{ value: 'off' }, { value: 'balanced' }, { value: 'full' }]}
bind:value={previewMode}
/>
<ItemStepper
label="Build workers"
icon={cpuGlyph as unknown as Snippet}
min={1}
max={16}
bind:value={workers}
/>
</ItemGroup>proof
Size contract & control integration
fit puts an end lane on a DECLARED responsive width ladder — md 10rem→7rem→stack+100% (number/time class), lg 16rem→11rem→stack+100% (text/long-select class), full always 100% — stepped by the list's own container tiers (44rem mid, 30rem narrow). wrap=never stays the fixed lane: toggle/checkbox/badge never restack, and fit + wrap=never is a contract error that throws. controlChrome integrates the row as one large input-group: the field row owns the frame and its controls render BARE (the control-chrome ambient, explicit ?? ambient ?? frame — each family's own sheet paints its bare state, so a select never loses its chevron). The optical tail has ONE owner, and the inset vocabulary is the global practice — inset: 'auto' | number | boolean: auto infers from a terminal data-self-inset control (select always, input when clear/picker/suffix rides the edge, number always), a number hands the tail to the lane with exactly N px, boolean forces the inference on/off; folded rows keep the default tail (the contract suspends under the narrow law).
list-item · size contract
- lg — text class · the semantic glyph trails
- md — number class
- fixed — never restacks
- EscalationItemActions: flat physics + a joined cluster, DialogFooter semantics
<script lang="ts">
import {
Item,
ItemGroup,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemSelect,
ItemInput,
ItemToggle,
} from '@ui/list-item/index';
import PressButton from '@ui/press-button.svelte';
let channel = $state('Instantly');
let projectName = $state('');
let retentionDays = $state(30);
let autoplay = $state(true);
</script>
<!-- wide · the ladder rungs (fit md 10rem→7rem→stack, lg 16rem→11rem→stack) -->
<ItemGroup label="delivery">
<ItemSelect fit="lg" label="Digest channel" bind:value={channel}>
<option>Instantly</option>
<option>Daily digest</option>
<option>Off</option>
</ItemSelect>
<ItemInput type="url" fit="lg" label="Webhook URL" description="lg — text class · the semantic glyph trails" bind:value={projectName} placeholder="https://" />
<ItemInput type="number" fit="md" label="Auto-archive after" description="md — number class" bind:value={retentionDays} />
<ItemToggle label="Sync over cellular" description="fixed — never restacks" bind:checked={autoplay} />
</ItemGroup>
<ItemGroup label="actions">
<Item>
<ItemContent><ItemTitle>Escalation</ItemTitle><ItemDescription>ItemActions: flat physics + a joined cluster, DialogFooter semantics</ItemDescription></ItemContent>
<ItemActions label="Escalation actions">
<PressButton>Acknowledge</PressButton>
<PressButton>Snooze</PressButton>
<PressButton variant="tonal">Escalate</PressButton>
</ItemActions>
</Item>
</ItemGroup>
<!-- narrow · the 17rem pane steps the ladder to its stacked rung -->
<div class="max-w-[17rem]">
<ItemGroup label="narrow">
<ItemSelect fit="lg" label="Digest channel" bind:value={channel}>
<option>Instantly</option>
<option>Daily digest</option>
</ItemSelect>
<ItemInput type="number" fit="md" label="Auto-archive" bind:value={retentionDays} />
<ItemToggle label="Sync cellular" bind:checked={autoplay} />
</ItemGroup>
</div>proof
ItemField escape hatch
Any control the adapters don't cover composes through ItemField's typed control snippet: it receives the full wiring context (controlId, labelId, descriptionId, errorId, describedBy). labelMode=for (default) associates a labelable element natively — click-to-activate, zero row handlers; labelMode=text is for non-labelable controls — the span label plus aria-labelledby is the naming source. The row is never a synthetic button and never nests a second label element.
list-item · item field
- for-mode on a labelable input
- Non-labelable control text-mode: aria-labelledby names itcustom
<script lang="ts">
import { ItemGroup, ItemField } from '@ui/list-item/index';
</script>
<ItemGroup mode="plain" dividers="auto">
<ItemField id="xf-labelable" label="Custom slider" description="for-mode on a labelable input">
{#snippet control(f)}
<input type="range" id={f.controlId} aria-describedby={f.describedBy} />
{/snippet}
</ItemField>
<ItemField
id="xf-custom"
label="Non-labelable control"
description="text-mode: aria-labelledby names it"
labelMode="text"
>
{#snippet control(f)}
<div
class="inline-flex items-center gap-2 border border-border px-2 py-1 text-[0.75rem]"
role="status"
aria-labelledby={f.labelId}
>
custom
</div>
{/snippet}
</ItemField>
</ItemGroup>proof
Selection & links
selected is VISUAL ONLY — terminal-hover fill plus the inset 2px primary edge (menu-family keeps its own bezel law; the divergence is documented, not accidental). No aria-selected is ever emitted: navigation adds aria-current itself through attribute forwarding; composite widgets own their selection semantics. Link rows are anchors with native keyboard behavior; rows with actions are NOT links.
list-item · selection & links
- current page selected + consumer-authored aria-current="page"/docs/components/list-item
- sibling route hover paints terminal-hover — color only, the press law
- actions row (not a link) interactive descendants belong outside anchorsRetry
<script lang="ts">
import {
Item,
ItemGroup,
ItemDivider,
ItemEnd,
ItemAfter,
ItemChevron,
ItemMedia,
ItemContent,
ItemTitle,
ItemDescription,
ItemActions,
ItemHeader,
ItemFooter,
} from '@ui/list-item/index';
import IconButton from '@ui/icon-button/icon-button.svelte';
import Icon from '@ui/icon';
</script>
<ItemGroup label="navigation">
<Item href="#selection-links" aria-current="page" selected>
<ItemContent>
<ItemTitle>current page</ItemTitle>
<ItemDescription>selected + consumer-authored aria-current="page"</ItemDescription>
</ItemContent>
<ItemEnd><ItemAfter>/docs/components/list-item</ItemAfter></ItemEnd>
</Item>
<Item href="#selection-links">
<ItemContent>
<ItemTitle>sibling route</ItemTitle>
<ItemDescription>hover paints terminal-hover — color only, the press law</ItemDescription>
</ItemContent>
<ItemEnd><ItemChevron /></ItemEnd>
</Item>
<Item>
<ItemContent>
<ItemTitle>actions row (not a link)</ItemTitle>
<ItemDescription>interactive descendants belong outside anchors</ItemDescription>
</ItemContent>
<ItemEnd>
<IconButton iconOnly text="Retry" class="size-7!">
{#snippet icon()}<Icon name="rotateCcw" />{/snippet}
</IconButton>
</ItemEnd>
</Item>
</ItemGroup>proof
Recipes
Two compositions the family deliberately does NOT own: the accordion row composes the W3C-first Accordion (<details>/<summary> — native toggle, native SSR state, keyboard owned by the platform); the checkbox group is ItemGroup + ItemCheckbox with one shared form name. Swipeout, sortable, virtual lists, smart select stay out of v1 by design — scroll-virtual already exists for the data-engine side.
list-item · recipes
expandable row (accordion recipe) details/summary
The accordion family owns disclosure; the row inside is a plain Item composition. Open state survives SSR — no JS hydration required.
a second row
Each summary is its own details element — the group only adds the visual law.
native same-form participation
<script lang="ts">
import { ItemGroup, ItemCheckbox } from '@ui/list-item/index';
import Accordion from '@ui/accordion.svelte';
import AccordionItem from '@ui/accordion-item.svelte';
</script>
<!-- the accordion recipe: W3C-first details/summary, the row composes inside -->
<Accordion>
<AccordionItem>
{#snippet summary()}
<span class="flex min-w-0 flex-1 items-center justify-between gap-2">
<span>expandable row (accordion recipe)</span>
<span class="text-[10px] opacity-60">details/summary</span>
</span>
{/snippet}
<p>
The accordion family owns disclosure; the row inside is a plain Item
composition. Open state survives SSR — no JS hydration required.
</p>
</AccordionItem>
<AccordionItem>
{#snippet summary()}
<span>a second row</span>
{/snippet}
<p>Each summary is its own details element — the group only adds the visual law.</p>
</AccordionItem>
</Accordion>
<!-- the checkbox-group recipe: ItemGroup + ItemCheckbox, one shared name -->
<ItemGroup mode="plain" dividers="auto" label="checkbox group (one form name)">
{#each ['build', 'lint', 'test'] as const as step (step)}
<ItemCheckbox name="pipeline" value={step} label={step} />
{/each}
</ItemGroup>law
Usage
The composition contract in one sample: import the family from the registry barrel (@ui/list-item/index — per-part targets exist per file), render ItemContent always, add the optional slots your row needs. Interactive behavior never lives on the row itself — link it with href or push controls into ItemEnd; settings rows take the adapters.
<script lang="ts">
import {
Item,
ItemContent,
ItemTitle,
ItemDescription,
ItemEnd,
} from '@ui/list-item/index';
import IconButton from '@ui/icon-button/icon-button.svelte';
import Icon from '@ui/icon.svelte';
</script>
<!-- ItemContent is the required slot; media/end/header/footer are
presence-optional — their existence rewrites the grid template.
The row IS the raised surface: trailing press controls ride FLAT
and frameless (IconButton's ghost default) — the frame is the
row's, not each glyph's. -->
<Item variant="outline">
<ItemContent>
<ItemTitle>Deploy #482</ItemTitle>
<ItemDescription>main · 4f2a1c · 2 minutes ago</ItemDescription>
</ItemContent>
<ItemEnd>
<IconButton iconOnly text="Rerun deploy" class="size-7!">
{#snippet icon()}<Icon name="check" />{/snippet}
</IconButton>
<IconButton iconOnly text="More actions" class="size-7!">
{#snippet icon()}<Icon name="ellipsis" />{/snippet}
</IconButton>
</ItemEnd>
</Item>types
Row variants
Variant controls paint only; slot presence owns list-item geometry. Use auto for context-sensitive chrome, or choose an explicit surface.
list-item · variants
<script lang="ts">
import { Item, ItemContent, ItemTitle, ItemDescription } from '@ui/list-item/index';
</script>
<div class="grid gap-3 sm:grid-cols-2">
<Item variant="default"><ItemContent><ItemTitle>default</ItemTitle><ItemDescription>host surface</ItemDescription></ItemContent></Item>
<Item variant="outline"><ItemContent><ItemTitle>outline</ItemTitle><ItemDescription>framed row</ItemDescription></ItemContent></Item>
<Item variant="muted"><ItemContent><ItemTitle>muted</ItemTitle><ItemDescription>quiet slab</ItemDescription></ItemContent></Item>
<Item><ItemContent><ItemTitle>auto</ItemTitle><ItemDescription>resolved from context</ItemDescription></ItemContent></Item>
</div>a11y
Accessibility
ARIA
| Attribute | Value | Description |
|---|---|---|
| a | href rows | Use href for navigable rows. |
| selected | visual only | Does not emit aria-selected; add selection semantics to the owning pattern. |
theming
Density and tokens
| Token | Default | Source |
|---|---|---|
| --jx-row-min | density scale | density |
| --jx-stack | density scale | density |
| --jx-inset | density scale | density |
| --jx-gap | density scale | density |
| --jx-gap-content | density scale | density |
| --jx-gap-end | density scale | density |
| --jx-media-gutter | density scale | density |
| --jx-image | density scale | density |
| --jx-hit | density scale | density |
| --jx-text | density scale | density |
| --jx-text-secondary | density scale | density |
| --jx-line | density scale | density |
| --jx-line-secondary | density scale | density |
| --jx-icon | density scale | density |
| --jx-icon-optical | 0px | component |
| --jx-unit | density scale | density |
| --jx-avatar-md | image scale | component |
| --jx-item-column-gap | gap | component |
| --jx-item-row-gap | stack | component |
| --jx-item-media-size | image | component |
| --jx-item-media-gutter | media gutter | component |
| --jx-item-content-gap | gap-content | component |
| --jx-item-end-gap | gap-end | component |
axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The §3 CONCENTRIC chain joined the family: the frame supplies --jx-radius-effective (an explicit lane makes the group the anchor), rows at radius='auto' compute max(0px, R − P) × the §14 factor (item.css swapped its var(--radius) literals for the consumed form — the batch B card law).
ItemGroup · universal props
- radius="auto" computes R − P
- the auto row follows the frame
<ItemGroup radius={20} density="small">…</ItemGroup>api
Item props
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| variant | 'auto' | 'default' | 'outline' | 'muted' | 'auto' · Own default, not ambient | Controls visual chrome (auto resolves it from the group policy). Defaults: literal slot — own 'auto', ambient when a table freeze lands. |
| layout | ItemLayout | 'auto' | Selects row layout mode. |
| selected | boolean | false | Visual selection state only. |
| href | string | — | Renders the root as an anchor. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |