registry:ui · Layout
card — the structural surface
THE STRUCTURAL KERNEL (2026-09-09): the head/body/foot bands, the stamped presence, the structural separators, the body-only scroll law — and the inline ruler everything rents (passive text enters the content axis by TRACK, interactive clusters ride the card edge flush). Structure is a STICKER: every rule anchors on the data-jx-card attribute family, so ANY element stamped with it — the planar Card root, a Dialog's interior host — acquires the whole ruler server-side by CSS alone; the named column lines and the jx-card container are public API. The content faces are the family parts: CardHeader (title/custom head), CardBody (the scroll cell — ONE gutter-compensation formula), CardFooter (the foot's seat law). Floating surfaces grow no flesh of their own — Dialog's interior speaks this dialect, its × riding the end-action seat reserved for it.
$npx jixoai-ui add card
https://ui.jixoai.com/r/card.json
usage
Usage
The card keeps the zones and the snippet transports; the content faces are composition — CardHeader for the head, CardFooter for the foot.
<script lang="ts">
import Card from '@ui/card.svelte';
import CardHeader from '@ui/card-header.svelte';
import CardFooter from '@ui/card-footer.svelte';
</script>
<!-- the bare card: default head face + body; no foot, no action seat -->
<Card title="Deploy hooks.">
<p>Body content — the only scrollable zone.</p>
</Card>
<!-- the action seat: the inline-end slot dialog's × is designed for -->
<Card title="Session">
{#snippet actions()}
<IconButton text="Session options" iconOnly />
{/snippet}
<p>Body.</p>
</Card>
<!-- the foot: raw transport, the standard face is CardFooter (start
seat / auto one ButtonGroup / raw end slot). PITFALL (issue #7):
bare buttons in a RAW foot do NOT row themselves — they ride the
ruler's content span, one per row, end-justified (the fail-soft
default; before it they squeezed into the 14px inset track,
clipped to slivers). Wrap actions in CardFooter for the carved
cluster, or place your own grid lines if you truly own the ruler -->
<Card title="Cart">
{#snippet foot()}
<CardFooter label="cart actions">
{#snippet start()}
<span>3 items</span>
{/snippet}
<PressButton>Clear</PressButton>
<PressButton variant="fill">Checkout</PressButton>
</CardFooter>
{/snippet}
<p>Line items.</p>
</Card>card
The structural surface, live: toggle the foot zone, the inline-end action seat, and the body's scroll authority. Presence is stamped on the host (data-sep-head / data-sep-foot); absent zones never render — their rows collapse.
Session
The body zone is the only scroll ring is declared to fit (scroll={false}): the authority and the gutter retire together.
<Card title="Session"
scroll={false}>
{#snippet foot()}
<CardFooter label="card actions">
{#snippet start()}<span>3 items</span>{/snippet}
<PressButton>Cancel</PressButton>
<PressButton variant="fill">Save</PressButton>
</CardFooter>
{/snippet}
<p>The body zone is the only scroll ring.</p>
</Card>law
Foot flexibility — both edges are first-class
The seats place against the rented inline ruler: text seats (start, end) enter the content axis — their 14px inset is a ruler TRACK, not a utility — while the button cluster spans the end inset to ride the card edge flush (its buttons carry the rhythm internally, dialog's footer economy verbatim).
card · foot seats
start + grouped actions
Meta text on the content axis; the cluster rides the edge flush.
raw end slot
The end slot replaces the grouped arrangement — a text seat at the content axis, 14px shy of the edge.
<script lang="ts">
import Card from '@ui/card.svelte';
import CardFooter from '@ui/card-footer.svelte';
import PressButton from '@ui/press-button/press-button.svelte';
</script>
<Card title="start + grouped actions">
{#snippet foot()}
<CardFooter label="cart actions">
{#snippet start()}<span>3 items · $42.00</span>{/snippet}
<PressButton>Clear</PressButton>
<PressButton variant="fill">Checkout</PressButton>
</CardFooter>
{/snippet}
<p>Meta text on the content axis; the cluster rides the edge flush.</p>
</Card>
<Card title="raw end slot">
{#snippet foot()}
<CardFooter>
{#snippet end()}<span>auto-saved 2m ago</span>{/snippet}
</CardFooter>
{/snippet}
<p>The end slot replaces the grouped arrangement — a text seat at the content axis, 14px shy of the edge.</p>
</Card><script lang="ts">
import Card from '@ui/card.svelte';
import CardHeader from '@ui/card-header.svelte';
import CardFooter from '@ui/card-footer.svelte';
</script>
<!-- the bare card: default head face + body; no foot, no action seat -->
<Card title="Deploy hooks.">
<p>Body content — the only scrollable zone.</p>
</Card>
<!-- the action seat: the inline-end slot dialog's × is designed for -->
<Card title="Session">
{#snippet actions()}
<IconButton text="Session options" iconOnly />
{/snippet}
<p>Body.</p>
</Card>
<!-- the foot: raw transport, the standard face is CardFooter (start
seat / auto one ButtonGroup / raw end slot). PITFALL (issue #7):
bare buttons in a RAW foot do NOT row themselves — they ride the
ruler's content span, one per row, end-justified (the fail-soft
default; before it they squeezed into the 14px inset track,
clipped to slivers). Wrap actions in CardFooter for the carved
cluster, or place your own grid lines if you truly own the ruler -->
<Card title="Cart">
{#snippet foot()}
<CardFooter label="cart actions">
{#snippet start()}
<span>3 items</span>
{/snippet}
<PressButton>Clear</PressButton>
<PressButton variant="fill">Checkout</PressButton>
</CardFooter>
{/snippet}
<p>Line items.</p>
</Card>composition
In a card grid — the foot mode
CardGrid's foot prop declares the third shared row: headers align, bodies fill to the tallest, feet align at the band bottoms. The integer cell placement resolves in every wrapped band.
card · grid foot mode
usage
One install, three files: the surface, the head face, the foot face.
anatomy
Head zone, edge-riding separator, the scroll ring, the foot seat.
the longer body
This body is deliberately taller — the other two cards' bodies stretch to match it through the shared 1fr row, and every foot lands on the same band bottom line.
<script lang="ts">
import Card from '@ui/card.svelte';
import CardFooter from '@ui/card-footer.svelte';
import CardGrid from '@ui/card-grid.svelte';
import PressButton from '@ui/press-button/press-button.svelte';
</script>
<CardGrid foot min="240px">
<Card title="usage">
{#snippet foot()}
<CardFooter>{#snippet end()}<span>npx jixoai-ui add card</span>{/snippet}</CardFooter>
{/snippet}
<p>One install, three files: the surface, the head face, the foot face.</p>
</Card>
<Card title="anatomy">
{#snippet foot()}
<CardFooter><PressButton variant="fill">Save</PressButton></CardFooter>
{/snippet}
<p>Head zone, edge-riding separator, the scroll ring, the foot seat.</p>
</Card>
<Card title="the longer body">
{#snippet foot()}
<CardFooter>
{#snippet start()}<span>rev 2</span>{/snippet}
<PressButton>diff</PressButton>
</CardFooter>
{/snippet}
<p>
This body is deliberately taller — the other two cards' bodies stretch to match it
through the shared 1fr row, and every foot lands on the same band bottom line.
</p>
</Card>
</CardGrid><CardGrid foot min="220px">
<Card title="one">
{#snippet foot()}<CardFooter>{#snippet end()}<span>meta</span>{/snippet}</CardFooter>{/snippet}
<p>Body fills to the tallest; the foot aligns at the band bottom.</p>
</Card>
<Card title="two">
{#snippet foot()}<CardFooter><PressButton variant="fill">Save</PressButton></CardFooter>{/snippet}
<p>Second card.</p>
</Card>
</CardGrid>types
Types
Three postures: the bare card, the chrome-less body card, and the full zone trio.
card · variants
Deploy hooks.
No foot, no action seat — the stamps stay honest.
Neither title nor a head snippet: no head zone, no head separator.
Session
The zone trio a card-grid foot band equalizes.
<script lang="ts">
import Card from '@ui/card.svelte';
import CardFooter from '@ui/card-footer.svelte';
import PressButton from '@ui/press-button/press-button.svelte';
</script>
<Card title="Deploy hooks.">
<p>No foot, no action seat — the stamps stay honest.</p>
</Card>
<Card>
<p>Neither title nor a head snippet: no head zone, no head separator.</p>
</Card>
<Card title="Session">
{#snippet foot()}
<CardFooter>
{#snippet start()}<span>3 items</span>{/snippet}
<PressButton variant="fill">Save</PressButton>
</CardFooter>
{/snippet}
<p>The zone trio a card-grid foot band equalizes.</p>
</Card>a11y
Accessibility
The card is a generic section container; the separators are decorative chrome, the zones carry no semantics of their own.
Keyboard
| Key | Action |
|---|---|
| Escape / close | Not applicable — no close affordance ships; the action seat is the consumer’s own control |
ARIA
| Attribute | Value | Description |
|---|---|---|
| root | <section> | A generic container; give it an accessible name via its content when the region is navigable |
| separators | aria-hidden | The structural lines are decorative — hidden from AT |
| action seat | consumer-owned | Whatever sits in the actions slot keeps its own role, name, and activation behavior |
theming
Theming
The inline ruler, verbatim: the root owns five named tracks ([inset] 14px · [start seat] auto · [fill] minmax(10px,1fr) · [end seat] auto · [inset] 14px); head/foot zones rent them via subgrid. Zones paint nothing; no face carries an inline padding utility — tracks paint the axis. The card is not density-scaled.
| Token | Default | Source |
|---|---|---|
| text seats (head title, foot start/end) | content axis (track) | structural |
| edge riders (actions slot, foot cluster) | flush, span to -1 | structural |
| body cell | py-3.5 + inline compensation | component |
| --card-foreground | theme | color |
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 card is the §3 concentric ANCHOR: an explicit radius supplies --jx-radius-effective while the ruler's inline inset track supplies --jx-inset-effective — descendants at radius='auto' compute max(0px, R − P), and shape='squircle' doubles the resolved corner through the §14 factor ladder (reversing on degrade).
Card · universal props
radius 20 · the concentric anchor
children at radius="auto" compute max(0px, 20 − 14) = 6px
shape squircle · radius 20
the §14 factor doubles the resolved corner (and reverses on degrade)
named step
var(--jx-radius-large) — the §12 indirection, zero inline values
size 14 · density small
one number moves the family; the rung scopes the channels
<!-- the concentric anchor: radius 20 + the 14px ruler inset →
the auto-radius button computes max(0px, 20 − 14) = 6px -->
<Card title="anchor" radius={20}>
<PressButton radius="auto">concentric child</PressButton>
</Card>
<Card title="squircle" shape="squircle" radius={20}>×2 through the factor ladder</Card>
<Card title="named step" radius="large">var(--jx-radius-large) — zero inline values</Card>api
API
Card keeps the bands and the snippet transports; CardHeader / CardBody / CardFooter are the content faces — usable standalone too (their own ruler mirrors carry them outside any band).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| title | string | — | Heading of the default head face (CardHeader). Omit with no head snippet for a chrome-less card. |
| head | Snippet | — | Raw head override — enters at the content axis (typically wraps CardHeader). |
| actions | Snippet | — | The inline-end action seat — dialog × position, edge-riding flush. Absent, the slot never renders; no close button ships. |
| foot | Snippet | — | Raw full override of the foot zone; the standard face is CardFooter. |
| scroll | boolean | true | Body scroll authority; false retires it together with the gutter reservation. |
| class | string | '' | Root utilities appended after the law’s own. |
| children * | Snippet | — | The body — the only scrollable zone. |
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) |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| title | string | — | CardHeader: the default title row (h2, py-2.5 — the inline inset is the ruler’s track); yields to children. |
| children | Snippet | — | CardHeader: custom head content at the content axis — owns its own block geometry. |
| class | string | '' | CardHeader: appended to the face — the FLUSH escape hatch: class='col-start-1' pins the column start to the grid's first line (flush past the inset track) while the × seat keeps its row (the search-field head idiom). |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| start | Snippet | — | CardFooter: the inline-start TEXT seat — the content axis. |
| end | Snippet | — | CardFooter: the inline-end TEXT seat; replaces the grouped arrangement. |
| label | string | 'Actions' | CardFooter: the ButtonGroup accessible name (neutral since the kernel — the footer family serves every carrier). |
| children | Snippet | — | CardFooter: action buttons — the CLUSTER seat: one leadingSeam ButtonGroup riding the card edge flush. |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| scroll | boolean | true | CardBody: the band scroll authority (the host never scrolls) — the cell is the ring with a stable both-edges gutter; false retires the authority and the gutter together. |
| children * | Snippet | — | CardBody: the content — enters at the cell content axis, the single-sourced gutter-compensation formula padding it. |