grammar
The grammar — one law, eight axes
Every component accepts the same eight style axes. Each axis takes a NAMED step (theme aliases, developer-remappable), the reserved literal auto (inherit the ambient context — the default, stamps nothing), an exact NUMBER (the unit is per-axis: px · coefficient · dp · hue), or query() wrapping any lane for responsive and container-conditional values. Values resolve to CSS expressions stamped as custom properties on the component root — never new classes.
size · shape · radius · density · color · theme · elevation · motion
lanes
The three lanes + the wrapper
named | auto | ${number} on every axis, plus the orthogonal query() wrapper — the reserved literals (auto, numbers) can never be remapped or shadowed.
| Property | Type | Default | Description |
|---|---|---|---|
| named | 'small' | 'medium' | 'large' | … | per axis | A theme alias step — resolves through the plugin alias table as var(--jx-<axis>-<alias>), developer-remappable by overriding the var (pure CSS, no resolver runtime). The documented vocabulary per axis lives in the table below. |
| auto | 'auto' | 'auto' | RESERVED LITERAL — inherit the resolved ambient context (explicit prop > ambient > the family default). The DEFAULT of every axis; an auto resolution stamps nothing, the context flows. May not be remapped or shadowed. |
| ${number} | number | per axis | RESERVED LITERAL — the exact-value escape. The unit is per axis: px for size/radius, a coefficient for density/motion, dp for elevation, hue degrees for color. shape/theme have no number lane. |
| query() | QueryResult<T> | base | The orthogonal wrapper — responsive and container-conditional values on ANY axis, wrapping ANY lane. See the query() section: media keys bare (sm), container keys carry @ (@sm), named containers @sm/card. |
axes
The eight axes
The frozen lane table (the docs vocabulary; the legacy density spellings keep working as aliases).
| 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) |
query()
query() — conditional values
query({ [condition]: <lane value> }) wraps ANY lane of ANY axis. Media conditions are bare (sm · md · lg — xs|sm|md|lg are RESERVED for this use); container conditions carry @ (@sm) on their OWN --container-* scale — @sm = 24rem while viewport sm = 40rem, the two never collapse. A NAMED container addresses as @sm/card: size first, then /, then the name (Tailwind v4's own order). The ladder is min-width, later keys override at wider matches; the desugarer emits blocks in REGISTERED-SCALE order so authoring order never matters ({lg:'a', sm:'b'} and {sm:'b', lg:'a'} compile identically). Containers resolve against the NEAREST qualifying ancestor — a component cannot query itself — and trees that use @ keys need an ancestor with container-type (the broadcast supply duty; the canvas's scroll layer is one).
| Property | Type | Default | Description |
|---|---|---|---|
| sm | 'xs' | 'sm' | 'md' | 'lg' | — | MEDIA keys are bare — the viewport vocabulary, xs|sm|md|lg RESERVED for this use. The registered default table: xs 30rem · sm 40rem · md 48rem · lg 64rem (plugin-remappable thresholds). |
| @sm | '@3xs' | '@2xs' | '@xs' | '@sm' | '@md' | '@lg' | '@xl' | — | CONTAINER keys carry @ (the Tailwind v4 @container convention) and ride their OWN --container-* scale: @sm = 24rem ≠ viewport sm = 40rem. Resolves against the NEAREST qualifying ancestor container — a component cannot query itself (CSS law). |
| @sm/card | `@${scale}/${string}` | — | A NAMED container — size first, then /, then the container name. Contributors of containers stamp container-name + container-type (the §11 supply duty); a missing name never matches and the build warns. |
| base | T | undefined | 'auto' | The unconditional lane rendered at SSR first paint and when no case matches. Semantics: min-width ladder, later keys override at wider matches — blocks emit in REGISTERED-SCALE order (narrow → wide), never raw insertion order. |
<!-- media keys are BARE; the ladder is narrow → wide, insertion order never matters -->
<Card size={query({ sm: 'large' })}>…</Card>
<!-- container keys carry @; a NAMED container is size/name -->
<PressButton radius={query({ '@sm': 20, '@md/sidebar': 8 })}>…</PressButton>
<!-- any lane wraps: named steps, numbers, and a base -->
<Card radius={query({ sm: 'large' }, 'small')}>…</Card>law
Resolution, supply & the carrier law — 吃也供
An axis resolves explicit prop → ambient context → the family's own default. A component that CONSUMES an axis also SUPPLIES its resolved value downward (the Svelte context carries the INTENT, the CSS custom properties carry the RENDER — two channels of one resolution), so a nested component inherits by default: an inner radius='auto' computes max(0px, R − P) off the ancestor's --jx-radius-effective/--jx-inset-effective (the concentric broadcast; the var() fallbacks are load-bearing). Every resolved value is a CSS EXPRESSION — vars are the degenerate case, calc()/max()/color-mix() the general case — assigned to custom properties on the component root: SSR/zero-JS by construction, zero new class identities, stylex never sees instance values. On native-element wrappers the family consumes size and color — the native input never receives them; everything the family does not own still rides through rest props.
Every migrated family's docs page carries the live Universal props section and demo — the native form
families (input, native-select, textarea, checkbox, radio, range, color-picker,
file-input, number-input, cascader, tags-input, input-otp, combobox, input-group,
date-picker, ghostty-term) carry the surface first, and the component-canvas wires the
per-axis CONTROLS (enum select · number spinner · query() editor) onto its dock.
degrade
The degrade tables — designed states, never silent luck
Any new-CSS capability gets a plugin-level @supports verdict, one global treatment, a documented fallback table. corner-shape is the first instance: the verdict stamps VARS, never classes — the ladder ships twice gated by CSS itself, families read the ladder and never the capability. squircle's ×2 law rides the per-shape factor var (--jx-radius-factor-squircle: 2 supported · 1 degraded) so its reversal is automatic; scoop/bevel/notch factor to their degrade target's geometry (square ⇒ factor 0).
corner-shape unsupported →
| Property | Type | Default | Description |
|---|---|---|---|
| scoop | 'scoop' | square | Degrades to square geometry — the radius factor zeroes so the corner actually squares (a non-zero radius under a square corner-shape still rounds). |
| bevel | 'bevel' | square | Degrades to square geometry (factor 0). |
| notch | 'notch' | square | Degrades to square geometry (factor 0) — the only safe target (§15.1). |
| squircle | 'squircle' | round | Degrades to round AND the ×2 law reverses: --jx-radius-factor-squircle steps 2 → 1, the same var, zero branches. |
| round | 'round' | round | Native border-radius semantics — itself in both worlds, factor 1/1. |
| square | 'square' | square | Itself; the radius lane is inert by definition (factor 0). |
Existing component-level probes are REGISTERED EXCEPTIONS absorbed through the ladder vars (press-effect-runtime, avatar) or filed in the gate's exemption ledger with reasons (the squircle percentage boundary) — a degrade is a DESIGNED state with receipts.
dogfood
Live — the fleet on the axes
The migrated components, live: the card is the §3 concentric ANCHOR (radius 20 + the 14px ruler inset → descendants at auto compute 6px), the media-key query() demo steps the whole card family at the 40rem boundary, and the site's own docs infra (batch D4) rides the axes below.
radius 20 · the concentric anchor
children at radius="auto" compute max(0px, 20 − 14) = 6px
size query({ sm: 'large' })
below 40rem the base (auto) applies — radius too returns to the root 0px invariants (the square corners ARE the auto state); at ≥40rem the root font-size steps to the large rung — resize the window and watch the WHOLE family follow
shape squircle · radius 20
the §14 factor doubles the resolved corner — and reverses to ×1 on degrade, the same var.
site
The site itself — docs infra on the axes
The docs infrastructure families (token-table, props-table, a11y-table, density-demo, docs-pager, docs-sections-nav, search-palette) joined the same surface with batch D4 — their universality is site-side only (no registry item), demonstrated here.
| Token | Default | Source |
|---|---|---|
| --jx-size-effective | 18px (this table) | component |
| --jx-density-coefficient | 1 (named rung) | density |
| Property | Type | Default | Description |
|---|---|---|---|
| size | SizeLane | QueryResult<SizeLane> | auto | This table renders at the named medium step — var(--jx-size-medium) through the alias ladder, zero inline px. |