jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

auto-variant chrome native ul/li groups ItemEnd trailing lane ItemField + 7 adapters grid + :has() presence matrix

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.

Deploy #482 main · 4f2a1c · 2 minutes ago
list-item-usage.svelte svelte
<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

auto · surface
the auto row data-item-chrome="surface"
own frame
variant="default" explicit — always wins over context
variant="outline" explicit — always wins over context
variant="muted" explicit — always wins over context
density
size="default" padding, gaps and type scale — never geometry
size="sm" padding, gaps and type scale — never geometry
size="xs" padding, gaps and type scale — never geometry
list-item-standalone-ladder-demo.svelte svelte
<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

registry
strong relations
  • muted slab terminal-muted fill · no frame · no dividers, ever
  • rows read as one surface
    2
host-owned (plain)
  • plain group nothing painted — this page is the surface; hairlines opted in
  • for menus, panels, terminals
inset
  • inset group fixed 0.75rem inline margins — boolean, no responsive enum
  • same paint as default
    fixed geometry
list-item-group-modes-demo.svelte svelte
<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

registry folder 12 components · updated today
3 open edit
AL
Ada Lovelace maintainer · 3 commits today
12:04
press-button blueprint
press-button the physical press law · blueprint
pull request #2182 open
feat: the presence matrix @gaubee wants to merge 3 commits into main
3 checks passed · 2 files changed updated 14 minutes ago
list-item-slot-topology-demo.svelte svelte
<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

  • G
    media layout rows media self-starts, content gap loosens — the custom-property switch
    compiler
  • A
    works for feeds and threads the same four presence bits, a calmer rhythm
    dynabook
narrow group (container ≤ 30rem): the end lane takes its own row
list-item-media-narrow-demo.svelte svelte
<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

lg
  • the lg row · 15px text · 24px line · 48px
    hit 44px
default
  • the default row · 13px · 20px · 40px
    hit 44px
sm
  • the sm row · 12px · 18px · 32px
    hit 44px
xs
  • the xs row · 11px · 16px · 28px
    hit 44px
list-item-density-ladder-demo.svelte svelte
<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

workspace
  • 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

list-item-settings-demo.svelte svelte
<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

wide · the ladder rungs
delivery
  • lg — text class · the semantic glyph trails
  • md — number class
  • fixed — never restacks
actions
  • EscalationItemActions: flat physics + a joined cluster, DialogFooter semantics
17rem pane · the narrow rung
narrow
list-item-size-contract-demo.svelte svelte
<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 it
    custom
list-item-item-field-demo.svelte svelte
<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

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.

checkbox group (one form name)

native same-form participation

list-item-recipes-demo.svelte svelte
<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.

usage
<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

defaulthost surface
outlineframed row
mutedquiet slab
autoresolved from context
list-item-types-demo.svelte svelte
<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

AttributeValueDescription
ahref rowsUse href for navigable rows.
selectedvisual onlyDoes not emit aria-selected; add selection semantics to the owning pattern.

theming

Density and tokens

xs
density rowscoped
default
density rowscoped
lg
density rowscoped
TokenDefaultSource
--jx-row-mindensity scaledensity
--jx-stackdensity scaledensity
--jx-insetdensity scaledensity
--jx-gapdensity scaledensity
--jx-gap-contentdensity scaledensity
--jx-gap-enddensity scaledensity
--jx-media-gutterdensity scaledensity
--jx-imagedensity scaledensity
--jx-hitdensity scaledensity
--jx-textdensity scaledensity
--jx-text-secondarydensity scaledensity
--jx-linedensity scaledensity
--jx-line-secondarydensity scaledensity
--jx-icondensity scaledensity
--jx-icon-optical0pxcomponent
--jx-unitdensity scaledensity
--jx-avatar-mdimage scalecomponent
--jx-item-column-gapgapcomponent
--jx-item-row-gapstackcomponent
--jx-item-media-sizeimagecomponent
--jx-item-media-guttermedia guttercomponent
--jx-item-content-gapgap-contentcomponent
--jx-item-end-gapgap-endcomponent

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

concentric
  • radius="auto" computes R − P
  • the auto row follows the frame
list-item-universal.svelte svelte
<ItemGroup radius={20} density="small">…</ItemGroup>

api

Item props

Properties

PropertyTypeDefaultDescription
variant 'auto' | 'default' | 'outline' | 'muted''auto' · Own default, not ambientControls 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 booleanfalseVisual selection state only.
href string—Renders the root as an anchor.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h