registry:ui · Data Display
heading — the ladder emigrated from the face
A native h1–h6 by level (1–6, rounded then clamped), carrying the em size ladder that used to live in the markdown prose face: 1.875em at h1 down through 1em at h5/6. em, never rem — the hierarchy scales with the ambient font-size preset and survives the escape from any face. What it owns: bold, leading 1.25, the foreground ink, the size. What it refuses: a paint ladder (level IS the structural axis), block margins, and a Defaults file (level is not a style prop).
$npx jixoai-ui add heading
https://ui.jixoai.com/r/heading.json
overview
Overview
One clamped value derives everything: the native element, the em rung and the hook never disagree.
Heading renders one native element — h1 through h6 via svelte:element — from a single clamped level: round to the
nearest integer, bound to 1–6, then derive the tag, the em-scaled size rung and the data-jx-heading hook from that ONE value, so an out-of-range prop never
desyncs the rendered truth from anything. Default 2 — h1 is the page's one-per-page
title, and a component defaulting to it would mint competing titles; the markdown map
always passes level explicitly.
The em ladder is em, never rem: h1 1.875em → h2 1.5em → h3 1.25em → h4 1.125em → h5/h6 1em, of the ambient font-size (the typography trio presets bump it; the whole hierarchy rescales). No block margins — preflight zeroes them and the component stamps none back; the container rhythm owns spacing. No Defaults file: level is structural, not a style prop (the separator literal-axis precedent). Kept outside the eight axes per migration-census §13.
The paint is exactly the face's B1 heading channels — bold, leading 1.25, and the ink
seam var(--jx-ty-ink, var(--foreground)) — so the component works
standalone AND rejoins the prose face by hook: inside a data-jx-ty-ink='gradient' region the face fills the glyphs through data-jx-heading; the component owns no gradient knowledge. A leaf in the
fleet (grep receipt: only the blueprint scenes mount it) — composition is
consumer-side. The eight axes resolve on the h root: size is the one axis that
repaints (via its stamp), theme is the partial pole (ink flips, ladder frozen), and
six stamp-and-supply. Per-axis below; the shared grammar lives on the universal props page.
usage
Usage
Hand it a level; the element, the size and the hook all derive from the one clamped value — the tag and data-jx-heading never disagree.
<script lang="ts">
import { Heading } from '@ui/heading';
</script>
<!-- the ladder: level sets the element AND the em-scaled size -->
<Heading level={2}>The section title</Heading>
<Heading level={3}>A subsection</Heading>
<!-- standalone: the hierarchy scales with the ambient font-size preset
(em, never rem) and carries no margins of its own -->
<div class="flex flex-col">
<Heading level={4}>Tight composition</Heading>
<p>The container rhythm owns the spacing.</p>
</div>heading
The level ladder, rendered: each level is the native element at its em-scaled size with the same bold 1.25 leading and foreground ink. The demo spans levels 2–6 — a demo-minted h1 would compete with the page's single hero title (the one-h1 law); level 1 renders at 1.875em.
h2 · 1.5em The level 2 rung
h3 · 1.25em The level 3 rung
h4 · 1.125em The level 4 rung
h5 · 1em The level 5 rung
h6 · 1em The level 6 rung
<script lang="ts">
import { Heading } from '@ui/heading';
</script>
<!-- the ladder: level sets the element AND the em-scaled size -->
<Heading level={2}>The section title</Heading>
<Heading level={3}>A subsection</Heading>
<!-- standalone: the hierarchy scales with the ambient font-size preset
(em, never rem) and carries no margins of its own -->
<div class="flex flex-col">
<Heading level={4}>Tight composition</Heading>
<p>The container rhythm owns the spacing.</p>
</div>demo
Standalone — outside any prose face
The component owns its channels outright, so it works with no jx-pure scope in sight: same ink, same weight, same ladder. It carries no margins — the container rhythm owns the spacing, GitHub's container-tight posture — and the markdown map escapes it (no-jx-pure) so the face stops double-painting it.
<!-- the six levels (level 1 clamps in; h1 is the page's
one-per-page title — components default to 2) -->
<Heading level={1}>1.875em — document title</Heading>
<Heading level={2}>1.5em — section</Heading>
<Heading level={3}>1.25em — subsection</Heading>
<Heading level={4}>1.125em — minor</Heading>
<Heading level={5}>1em — quiet label</Heading>
<Heading level={6}>1em — quietest</Heading>No face, no problem
The heading above rendered with zero prose-face context — the em ladder, weight and ink are the component's own. The gap you see is this container's rhythm, not the heading's margin.
api
Props
The table renders from the GENERATED meta + curation; the eight axis rows split into the shared section beneath. 14 meta props − 8 ambient axes = 6 rows; no EXTRA lane — level is the §13-kept structural prop.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| level | number | 2 | The heading level: sets the native element (h1–h6), the em-scaled size rung and the data-jx-heading hook from ONE clamped value (rounded, then bounded) — the rendered truth never desyncs from the raw prop. Default 2 — h1 is the page's one-per-page title, and a component defaulting to it would mint competing titles; the markdown map always passes level explicitly. Kept outside the eight axes per migration-census.md's §13 row (component-specific, no collision with the size axis). |
| id | string | — | Optional explicit address — anchor links and aria-labelledby point here. |
| style | string | null | — | Inline style passthrough — composed AFTER the family's carrier stamp (the #4 seam law: never clobbered, never dropped). |
| children | Snippet | — | The heading text. |
| class | string | '' | Forwarded to the rendered heading element; consumer classes land last. |
| rest | unknown (spread passthrough) | — | Every other attribute passes through to the native heading element untouched (Omit<…, 'color'> — the §1 native-collision rule). |
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 heading
heading is a typography primitive (census batch D): the paint is three fixed voices — weight, leading, the ink seam — and the em ladder, so six of the eight axes stamp-and-supply without a reader. The exceptions: size REPLACES the em rung through its carrier stamp (the only axis that repaints), and theme is the partial pole — the seam's --foreground fallback flips under a dark island while the ladder stays frozen. The carriers stamp the h root itself (the promoted root is self-carried), greppable in the raw SSR.
Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | CONSUMED VIA THE STAMP — the one axis that repaints. The §11 carrier emits BOTH --jx-size-effective AND the root's font-size (font-size: var(--jx-size-effective, 1rem)) inline on the h root, and inline wins the cascade — an explicit lane REPLACES the em rung (size={14} on an h3 renders exactly 14px; named steps resolve --jx-size-small/medium/large = 14/16/18px). auto stamps NOTHING and the em ladder rules: font-size = the level's rung × the ambient preset. Zero family css reads the var (grep receipt) — the stamp is the whole consumption. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY-ONLY — stamps --jx-shape-effective and --jx-radius-factor-effective; no family css reads them (grep receipt: zero readers). Text has no corners. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero readers) — the heading draws no boxes. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'auto' | SUPPLY-ONLY — the named rung stamps data-density and the lane resets --jx-density-coefficient; neither moves paint (grep receipt: zero --jx-density-* reads, and the --jx-text/--jx-line channels the rung scope re-bases are unread too — zero hits in ui/heading/). The heading's type voice is em-of-font-size, NOT the --jx-text channel: the density axis has no lever here, and a wrapper-level coefficient cannot re-base the rung (the declaring-element law). Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective; no family css reads it (grep receipt: zero readers — zero hue-carrier and zero oklch-formula reads). The ink rides the seam: var(--jx-ty-ink, var(--foreground)) — the theme's foreground, never the hue carrier. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE PARTIAL POLE — one raw-token voice flips. A resolved dark stamps .dark on the h root, and the root's OWN ink reads the raw token: color: var(--jx-ty-ink, var(--foreground)) re-substitutes against the .dark slot block and the heading re-inks (measured below). Everything else is theme-free and frozen: the weight, the leading and the em ladder carry no raw reads (grep receipt: the only raw token in ui/heading/ is --foreground). Inside a prose face with data-jx-ty-ink='gradient' the seam pins to --jx-ty-ink instead — the prose scope owns the ink and the island can't take it. light and system stamp nothing — tree inheritance. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (grep receipt: zero readers). Text casts no shadow. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. Zero transitions in the family — nothing animates, so the axis has nothing to step. Number unit: coefficient. |
Deviations, cited: the adoption is the census batch D row (the long tail —
openspec/changes/explicit-props/research/migration-census.md); level is KEPT outside
the axes per the §13 row (component-specific, no collision with the size axis — the
family's local em ladder even renamed from size to sizeLadder in the same ruling). The §1 collision rule: the native element never receives a size or
color attribute — Omit<HTMLAttributes<HTMLHeadingElement>, 'color'> clears the one native name at stake, and every axis name is the family's own
destructured lane.
<!-- size: an explicit lane RESOLVES the em rung — the §11
carrier stamps the root's font-size itself (inline wins the
cascade); auto keeps the ladder scaling with the ambient preset -->
<Heading level={3}>1.25 × the ambient preset</Heading>
<Heading level={3} size={14}>14px — the number is verbatim</Heading>
<Heading level={3} size="large">18px — named steps resolve --jx-size-*</Heading>
<!-- theme: the PARTIAL pole — a dark island re-inks through the
seam's --foreground fallback; the em ladder is theme-free -->
<Heading level={3} theme="dark">dark ink, same rung</Heading>heading · the consumed lane and the partial pole
1.25 × the ambient preset
14px — the number is verbatim
18px — the named step
dark ink, same rung
The top pair is the disagreement probe: same level, same tag — the unstamped h3 computes 1.25 × the inherited font-size while the stamped one computes exactly 14px (inline wins the cascade). The dark panel re-inks through the seam's --foreground fallback; its font-size, weight and leading are byte-identical to the light panels — the theme row's partial flip, not a full repaint.
<!-- size: an explicit lane RESOLVES the em rung — the §11
carrier stamps the root's font-size itself (inline wins the
cascade); auto keeps the ladder scaling with the ambient preset -->
<Heading level={3}>1.25 × the ambient preset</Heading>
<Heading level={3} size={14}>14px — the number is verbatim</Heading>
<Heading level={3} size="large">18px — named steps resolve --jx-size-*</Heading>
<!-- theme: the PARTIAL pole — a dark island re-inks through the
seam's --foreground fallback; the em ladder is theme-free -->
<Heading level={3} theme="dark">dark ink, same rung</Heading><script lang="ts">
import Heading from '@ui/heading.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (14px) applies; at 48rem+ the md case
(18px) wins and the h3 steps up -->
<Heading level={3} size={query({ md: 18 }, 14)}>responsive voice</Heading>heading · query()
responsive voice
Media keys are min-width: below 48rem the base applies — 14px; at 48rem and wider the md case wins — 18px. The number lane goes bare (results infer); it's the STRING lanes that need both generics. Resize across 48rem.
<script lang="ts">
import Heading from '@ui/heading.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (14px) applies; at 48rem+ the md case
(18px) wins and the h3 steps up -->
<Heading level={3} size={query({ md: 18 }, 14)}>responsive voice</Heading>| Token | Default |
|---|---|
| the em ladder | 1.875 / 1.5 / 1.25 / 1.125 / 1em |
| --jx-weight-bold | 700 |
| --jx-leading-tight | 1.25 |
| var(--jx-ty-ink, var(--foreground)) | --foreground fallback |
| --heading-size-1 … --heading-size-5 | unset (fallbacks = own values) |
| --jx-size-effective | stamped on explicit size |
a11y
Accessibility
Native heading semantics — the document outline is real. The id prop is the addressing surface anchor links and aria-labelledby point at.
Keyboard
| Key | Action |
|---|---|
| — | Not focusable — document structure, not a control |
ARIA
| Attribute | Value | Description |
|---|---|---|
| h1–h6 | native elements | The platform announces the level and builds the outline; skipping levels is the caller's outline decision, never silently repaired. |
| id | passthrough | Optional explicit address — the document addressing surface (anchor navigation, aria-labelledby). |
| data-jx-heading | clamped level | Hook attribute carrying the rendered level (1–6) — the clamped truth, never the raw prop. |