registry:ui · Data display
descriptions — composed dt/dd pairs, never a table
The enterprise detail view as a composed dl: Descriptions (columns/bordered HOW-props) + DescriptionsItem (term → dt, children → dd — rich cells are just children; childless items render the em dash).
$npx jixoai-ui add descriptions
https://ui.jixoai.com/r/descriptions.json
overview
Overview
The enterprise detail view, W3C-first: a dl IS a description list — dt/dd pairs in source order over a grid, one DescriptionsItem per pair. The bordered look is CSS on the same dl; columns and bordered are HOW-props — they change how the grid paints, never what renders.
One DescriptionsItem per pair: the term prop renders the
dt, children render the dd — rich cells are just children (badges, links, markup),
and a childless Item renders the em dash, never a blank cell. columns=N splits rows into N pairs per row, clamped 1–4 and responsive down to one pair per row
on narrow containers — the container query reads the dl's OWN container, never the
viewport, so any layout column inherits the law.
The anatomy rides the density ruler's ambient channels — the term's micro-label voice and the value's body voice re-tier with the ambient scope while the eight-axis carriers the root stamps have zero family readers: the chrome's theme voices are stylex aliases frozen at the :root pole (the avatar twin). The per-axis receipts are below.
usage
Usage
The composition contract in one sample: import the family from the registry barrel (@ui/descriptions/index — per-part targets exist per file). There is no items[] prop and no value snippet — rich cells are plain children of the Item.
<Descriptions columns={2} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
<DescriptionsItem term="status"><!-- rich cells are just children -->
<Badge>passing</Badge>
</DescriptionsItem>
<DescriptionsItem term="notes" /><!-- no children ⇒ the em dash -->
</Descriptions>descriptions
One Item per pair: the term prop renders the dt, children render the dd — the status badge is just children, the childless notes Item falls back to the em dash.
- build
- 4f2a1c
- checks
- 12 passed · 0 failed
- owner
- @gaubee
- region
- iad1 · washington
- status
- passing
- notes
- —
<Descriptions columns={2} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
<DescriptionsItem term="status"><!-- rich cells are just children -->
<Badge>passing</Badge>
</DescriptionsItem>
<DescriptionsItem term="notes" /><!-- no children ⇒ the em dash -->
</Descriptions>lane shapes
Description layouts
Descriptions is a semantic dl: one or more term/value pairs per row, the bordered treatment optional. This canvas composes its drawer from the stage markup itself — the code shown is the code running.
descriptions · types
- owner
- gaubee
- status
- live
- scope
- public
<script lang="ts">
import Descriptions from '@ui/descriptions/index';
import DescriptionsItem from '@ui/descriptions/index';
</script>
<div class={cx(rt.deGridMd2)}>
<div class={cx(rt.panel)}><Descriptions><DescriptionsItem term="owner">gaubee</DescriptionsItem></Descriptions></div>
<div class={cx(rt.panel)}><Descriptions columns={2} bordered><DescriptionsItem term="status">live</DescriptionsItem><DescriptionsItem term="scope">public</DescriptionsItem></Descriptions></div>
</div>examples
Examples
Ability-named recipes: vertical terms, responsive columns, extra header actions.
All three compose public structure — the Item's grid, the dl's container query, and a wrapper around the list. Nothing here reaches into the registry.
with vertical terms
antd's layout=vertical, composed: each Item re-runs its own grid as one column — the dt stacks above its dd. Same dl, same semantics, the term reads as a field label instead of a row lead. This drawer composes from the stage markup itself.
- owner
- @gaubee
- region
- iad1 · washington
- status
- passing
- build
- 4f2a1c
- runtime
- node 24 · bun
- notes
- —
<!--
jixoai DescriptionsItem (registry/files/ui/descriptions/descriptions-item.svelte,
composition-first-apis, 2026-08-25).
One term/value pair: the `term` prop renders the dt, children render
the dd — rich cells are just children (badges, links, markup). A
childless Item renders the em dash, never a blank cell. The bordered
hairline paint (cell bottom edge, muted term background + rule)
derives from the root's `bordered` through context — one decision,
painted everywhere.
tailwindless one-shot Wave 1b batch A (2026-09-17): the paint rides
the family's stylex ATOMS (descriptions.stylex.ts) joined through
cx() below.
-->
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { getContext } from 'svelte';
import { DESCRIPTIONS_KEY, type DescriptionsApi } from './descriptions.svelte';
import { descriptionsStyles } from './descriptions.stylex';
// the payload's own join (the separator serialize law): every
// stylex.create member is an OBJECT in dev and the joined string in
// shipped payloads — composition goes through THIS joiner (all
// string values except $$css, space-joined).
const cx = (
...styles: ({ readonly [key: string]: string | object } | undefined | string)[]
): string =>
styles
.filter(Boolean)
.map((style) =>
typeof style === 'string'
? style
: Object.entries(style ?? {}).flatMap(([key, value]) =>
key !== '$$css' && typeof value === 'string' ? [value] : [],
).join(' '),
)
.join(' ');
interface Props extends HTMLAttributes<HTMLDivElement> {
/** the dt label */
term: string;
class?: string;
children?: Snippet;
}
let { term, class: className = '', children, ...rest }: Props = $props();
const descriptions = getContext<DescriptionsApi>(DESCRIPTIONS_KEY);
</script>
<div
data-jx-desc-cell=""
class={cx(descriptionsStyles.cell, descriptions.bordered && descriptionsStyles.cellBordered, className)}
{...rest}
>
<dt
data-jx-desc-term=""
class={cx(descriptionsStyles.term, descriptions.bordered && descriptionsStyles.termBordered)}
>{term}</dt>
<dd data-jx-desc-value="" class={cx(descriptionsStyles.value)}>
{#if children}{@render children()}{:else}—{/if}
</dd>
</div>
with responsive columns
Drag the frame across the 640px line: the SAME three-column dl re-lays out into one pair per row — the container query reads the dl's own container, never the viewport, so any layout column inherits the law.
- build
- 4f2a1c
- checks
- 12 passed
- region
- iad1
- owner
- @gaubee
- runtime
- node 24
- status
- passing
<!-- the container query is the component's own law
(descriptions.css: @container (max-width: 640px) ⇒ one pair per row).
The consumer only owns the container's width: -->
<div style="width: {frameWidth}px">
<Descriptions columns={3} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<!-- … -->
</Descriptions>
</div>with extra header actions
antd's extra slot, composed honestly: a header row (title + actions) wraps the dl instead of living inside it — non-dl content never enters the list, the actions read back through the echo.
deploy · iad1 · production
- build
- 4f2a1c
- runtime
- node 24
- checks
- 12 passed · 0 failed
- owner
- @gaubee
last action: —
<!-- the extra slot composes AROUND the dl: a header row
(title + actions) over the list. antd's title/extra props would put
non-dl content INSIDE the dl — the wrapper keeps the semantics clean
(recorded in the change's followups). -->
<section class="border border-border">
<header class="flex items-center justify-between gap-3 px-4 py-3 border-b border-border">
<p class="m-0 font-nav text-xs uppercase tracking-[0.12em]">deploy · iad1</p>
<div class="flex gap-2">
<PressButton variant="outline" onclick={redeploy}>redeploy</PressButton>
<PressButton variant="ghost" onclick={rollback}>rollback</PressButton>
</div>
</header>
<Descriptions columns={2} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<!-- … -->
</Descriptions>
</section>props
Props
Props extend the native HTML dl attributes — minus color, whose name the eight-axis surface owns (the §1 native collision rule); the entries below are descriptions-specific additions, and everything else rides the rest object through to the dl.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| columns | number | 1 | Term/value pairs per row — clamped to 1–4 (truncated, never rounded up). The count stamps --jx-desc-cols on the root; the dl folds to ONE pair per row under 640px of CONTAINER width (the component's own container query — any layout column inherits the law). A HOW-prop: it changes how the grid paints, never what renders. |
| bordered | boolean | Own default, not ambient | The hairline "bordered" antd look — CSS on the same dl, never a table in disguise: the root grows the card-ground frame and the Items paint their cell bottom edges plus the muted term background + inline-end rule. Rides CONTEXT down to the Items — one decision, painted everywhere. |
| children * | Snippet | — | The DescriptionsItem pairs, in source order — the dl preserves description-list semantics, so nothing but dt/dd groups belongs inside. |
| style | string | '' | Inline style — joined AFTER the §11 axis carriers and the --jx-desc-cols stamp (the merge law: caller wins). |
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) |
axes
The eight axes on descriptions
The anatomy rides the density ruler's ambient channels: the term/value padding is --jx-gap/--jx-inset, the value's body voice is --jx-text/--jx-line, the term's micro-label is --jx-text-secondary/--jx-line-secondary — the named rung re-tiers the whole grid (measured 10/11/12/14 · 11/12/13/15 · 8/8/12/16 across xs/sm/default/lg; the number coefficient composes at :root — inert). size, shape, radius, color, elevation and motion stamp carriers nothing here reads (negative-grep receipt). Theme is the avatar frozen-pole twin: the chrome's five stylex aliases declare only at the :root,.xbpgcew pole + the theme class — the .dark bridge lands and nothing repaints (documented absence, W-next #1). Census: batch A (migration-census.md).
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | SUPPLY-ONLY — the root stamps --jx-size-effective plus an inline font-size, and every voice on the grid is density-channel-anchored (--jx-text / --jx-text-secondary), so the stamp reaches only unstyled flow under the root (the dt/dd never inherit it). The §1 collision applies: the dl never receives a size attribute. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | ambient scope | SUPPLY-ONLY — stamps --jx-shape-effective + --jx-radius-factor-effective; zero readers (grep receipt: zero carrier reads in ui/descriptions/). A detail grid has no corners of its own; the bordered frame is square-edged by the card law. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-radius-effective; zero readers (the same grep receipt). The bordered frame stays square; nothing computes the §3 concentric corner. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'xs' | '2xs' | 'sm' | 'default' | 'lg' | 'auto' | number | ambient scope | CONSUMED through the ambient scope (the no-own axis slot — no provider and no explicit prop resolve auto, stamp nothing, and the ambient css scope channel keeps flowing): the grid re-tiers end to end. Padding rides --jx-gap/--jx-inset, the value's body voice --jx-text/--jx-line, the term's micro-label --jx-text-secondary/--jx-line-secondary — measured 10/11/12/14px terms · 11/12/13/15px values · 8/8/12/16px padding across xs/sm/default/lg. The NUMBER lane is inert (the coefficient composes at the :root rung scopes; the wrapper never re-declares — measured: a 3× coefficient leaves 13px/12px unmoved). Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | ambient scope | SUPPLY-ONLY — stamps --jx-color-effective; zero family readers (grep receipt). A detail grid is content, not a painted object: the dt reads the muted alias, the dd the foreground alias, the bordered frame the border/card aliases — all theme voices, none of them the hue axis. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | ambient scope | THEME-FROZEN (the avatar twin, the documented absence W-next #1) — the root lands the .dark class bridge, and NOTHING repaints: every chrome voice is a stylex alias (--jx-border, --jx-card, --jx-muted, --jx-muted-foreground, --jx-foreground) whose declaration substitutes at the :root,.xbpgcew pole (built-CSS grep: each alias declares exactly twice — the pole + the theme class; never plain .dark). A documented absence: when the raw layer's dark values want in, the alias re-declaration is the named fix. light and system stamp nothing. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-elevation-effective; zero family readers (grep receipt). The bordered frame is a hairline on the card ground, no shadow layer; depth composes by the entity laws around the dl, never inside it. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | ambient scope | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it (grep receipt). A detail grid is a static read: nothing transitions, nothing moves — the reduced-motion law costs nothing here. Number unit: coefficient. |
Deviations, cited: the adoption is the census batch A row (explicit-props W3-D1 — the data-display families join the eight-axis surface; openspec/changes/explicit-props/research/migration-census.md). The §1 collision rule: the interface Omits 'color', the dl never receives an axis name. `bordered` keeps its literal slot (own false — booleans are a closed domain); `columns` is the family's own HOW-prop.
descriptions · the ruler and the frozen island
- build
- 4f2a1c
- owner
- @gaubee
- build
- 4f2a1c
- owner
- @gaubee
- build
- 4f2a1c
- owner
- @gaubee
- build
- 4f2a1c
- owner
- @gaubee
The number lanes stamp and re-base nothing here — the declaring element for every channel this family reads is the ambient scope (:root), the wrapper never re-declares (the declaring-element law). Measure the voices, not the coefficient: 10 / 11 / 12 / 14px terms, 11 / 12 / 13 / 15px values, 8 / 8 / 12 / 16px padding.
<script lang="ts">
import Descriptions from '@ui/descriptions/index';
import DescriptionsItem from '@ui/descriptions/index';
</script>
<div class={cx(rt.gridSm2, rt.wFull)}>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>density default — 13px values, 12px secondary terms, 12px padding</span>
<Descriptions bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>density="lg" — the touch tier steps the whole grid (15px, 16px padding)</span>
<Descriptions bordered density="lg">
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>theme="dark" — the bridge lands; the chrome stays frozen (nothing repaints)</span>
<Descriptions bordered theme="dark">
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>
</div>
<div class={cx(rt.panel)}>
<span class={cx(rt.note11)}>color="error" — stamped, unread: content is not a painted object</span>
<Descriptions bordered color="error">
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>
</div>
</div>
<p class={cx(rt.mt8, rt.note12, rt.inkMuted70)}>
The number lanes stamp and re-base nothing here — the declaring element for every
channel this family reads is the ambient scope (:root), the wrapper never
re-declares (the declaring-element law). Measure the voices, not the coefficient:
10 / 11 / 12 / 14px terms, 11 / 12 / 13 / 15px values, 8 / 8 / 12 / 16px padding.
</p><script lang="ts">
import Descriptions, { DescriptionsItem } from '@ui/descriptions/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<Descriptions density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>descriptions · query()
- build
- 4f2a1c
- owner
- @gaubee
Media keys are min-width: below 64rem the base applies — the small rung, the pointer-lane grid; at 64rem and wider the lg case wins and the grid steps to the touch tier (12px/8px padding below, 15px/16px above — measured). Resize across 64rem.
<script lang="ts">
import Descriptions, { DescriptionsItem } from '@ui/descriptions/index';
import { query } from '@lib/universal-props-query.svelte';
import type { DensityLane } from '@lib/defaults.svelte';
</script>
<Descriptions density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} bordered>
<DescriptionsItem term="build">4f2a1c</DescriptionsItem>
<DescriptionsItem term="owner">@gaubee</DescriptionsItem>
</Descriptions>| Token | Default | Source |
|---|---|---|
| --jx-desc-cols | columns prop (clamped 1–4) | component |
| --jx-gap | 8 / 8 / 12 / 16px across xs / sm / default / lg | density |
| --jx-inset | 8 / 8 / 12 / 16px across xs / sm / default / lg | density |
| --jx-text | 11 / 12 / 13 / 15px across xs / sm / default / lg | density |
| --jx-text-secondary | 10 / 11 / 12 / 14px across xs / sm / default / lg | density |
| --jx-line | the dd's leading rung | density |
| --jx-line-secondary | the dt's leading rung | density |
a11y
Accessibility
The dl preserves description-list semantics — wrappers add chrome, never list content; the term names the property, the value answers it.
ARIA
| Attribute | Value | Description |
|---|---|---|
| dl | Descriptions root | Preserves description-list semantics — wrappers add chrome, never list content. |
| dt | term | Names each property. |
| dd | value | Contains the corresponding value. |