The anatomy contract
Every rendered part of every component carries data-scope="<component>",
data-part="<part>" and at most one data-state value. That is the whole styling seam —
attributes, never classes or inline styles — and everything else in zero follows from it: a
design system can be swapped at runtime by swapping a stylesheet, a design system can be
generated by something that has only read the manifest, and a component the design system has
never heard of still renders correctly attributed, working and accessible.
Scope, part, state
<div data-scope="tabs" data-part="root" data-orientation="horizontal" data-color="primary">
<div data-scope="tabs" data-part="list" role="tablist">
<button data-scope="tabs" data-part="tab" data-state="active" role="tab">…</button>
<button data-scope="tabs" data-part="tab" data-state="inactive" data-disabled="" role="tab">…</button>
</div>
<div data-scope="tabs" data-part="panel" data-state="active" role="tabpanel">…</div>
<div data-scope="tabs" data-part="panel" data-state="inactive" hidden role="tabpanel">…</div>
</div>
data-scopeis the component name, kebab-case (tabs,radio-group,tree-view).data-partis the part name, kebab-case, from a closed per-component list.data-stateholds exactly one value at a time from a closed, per-part set.- Flags are boolean and presence-only:
data-disabled="", neverdata-disabled="false".
A design system selects on these: [data-scope="tabs"][data-part="tab"][data-state="active"].
Zero attaches no styling to any of it beyond a minimal structural css/base.css.
States are governed
data-state values come from one shared vocabulary, STATE_VOCABULARY
(@sigx/zero/contract), grouped into families:
| Family | Values |
|---|---|
| presence | open | closed |
| selection | checked | unchecked | indeterminate |
| activation | active | inactive |
| toggle | on | off |
| loading | loading | loaded | complete | error |
| fill | full | half | empty |
The families are documentation, not a per-part constraint: membership is checked against
the union, so progress legitimately mixes loading / complete with indeterminate.
Every state a component's anatomy declares must be a member, and a new state value is a
contract change.
A companion STATE_SYNONYMS table names the spellings the vocabulary deliberately does
not contain and the member that means the same thing — expanded → open,
collapsed → closed, mixed → indeterminate, current → active, busy → loading,
done → complete, failed → error, and so on. It is diagnostic: a governance failure names
the right word instead of leaving you to guess.
Flags are a closed vocabulary
FLAG_VOCABULARY: disabled, highlighted, selected, invalid, required,
readonly, placeholder, focus-visible, pressed, press-animating.
Components never invent synonyms. Each part declares which flags it can carry, and every
one renders presence-only. focus-visible mirrors the platform's :focus-visible heuristic
as an attribute so a design system can draw the ring on a different element than the one that
has focus (a combobox's control box, a switch's visible track). pressed and
press-animating are the press-feedback pair.
Design-system modifiers live in a separate, prefixed namespace — data-mod-<name> — so they
can never collide with a flag zero adds later. See Variant axes.
Placements are declared
PLACEMENT_VOCABULARY closes data-placement: the twelve side/alignment values (top,
top-start, top-end, bottom…, left…, right…) plus the bare logical pair start and
end. A part that can carry the attribute declares which subset in its anatomy
(PartSpec.placements): the anchored-position behavior stamps open popups with where they
actually landed after flipping, Toast stamps its viewport and roots, Indicator.Item,
Timeline.Content, Chat.Root and Drawer.Panel stamp the logical side they sit on.
The part tree
PartSpec.parent names the same-scope part a part renders inside — the anatomy is a
tree whose roots are the top-level parts. parent is a statement about the rendered DOM,
not the compound-component API, and it names the containing part, not the immediate
parent element: a menu item declares parent: 'popup' even when it renders inside a
group, because other parts and consumer markup may sit between.
Three consumers read the tree: expectAnatomy asserts the declared parent appears among an
element's same-scope ancestors; the kit's contrast audit derives real ancestor chains from
it; and the recipe compiler uses containment to bound descendant-anchored axis rules and to
detect rules that can never match.
The carrier part
Axis attributes (data-color, data-size, data-variant, data-<axis>, data-mod-*)
live on one part per scope, the carrier: the part named root, else the first declared
part. Four scopes have no root — Dialog, Menu, Popover and Tooltip render a fragment Root —
so their carrier is the trigger, and their axis props live on X.Trigger. Because their
popups are top-layer siblings of the trigger, axis rules can never reach the popup; colour on
those scopes styles the trigger.
hiddenIn is a styling fact
A part the runtime hides with the hidden attribute in some state declares those states:
hiddenIn: ['inactive'] on tabs.panel, ['error'] on avatar.image, ['loaded'] on
avatar.fallback, ['closed'] on alert.root and tree-view.branch-content. It belongs in
the anatomy because it changes what a recipe can honestly be asked to do: a rule targeting a
hidden state can never paint, so a design system may leave it unstyled and need not tell it
apart from a visible state.
The hiding is enforced. css/base.css declares a fourth cascade layer,
zero.structure, holding one rule —
[data-scope][data-part][hidden]:not([hidden="until-found" i]) { display: none } — after
zero.recipes, so an unconditional display: flex in a recipe cannot defeat it. A recipe
that needs display on such a part writes it under &:not([hidden]). Unlayered app CSS
still wins; hidden="until-found" is exempt so find-in-page keeps working.
Pseudo parts
A part that renders no element of its own declares pseudo: { of, selector } — the
dialog's backdrop is { of: 'popup', selector: '::backdrop' }. Selectors compose with the
pseudo-element last, so states narrow the host: [data-part="popup"][data-state="open"]::backdrop.
The part stays real in the anatomy because a platform without a native top layer renders it
as an element.
The anatomy in code
Every component's anatomy.ts is its source of truth; the component imports its part names
from it, tests assert against it, and the build emits it into the manifest:
import { defineAnatomy } from '@sigx/zero/anatomy';
export const tabsAnatomy = defineAnatomy('tabs', {
root: { element: 'div', tokens: ['color'] },
list: { element: 'div', parent: 'root', tokens: ['color', 'radius-field'] },
tab: {
element: 'button', parent: 'list',
states: ['active', 'inactive'],
flags: ['disabled', 'focus-visible', 'pressed', 'press-animating'],
tokens: ['color', 'radius-field', 'size', 'text'],
asChild: true,
},
panel: {
element: 'div', parent: 'root',
states: ['active', 'inactive'],
hiddenIn: ['inactive'],
tokens: ['color', 'radius-box', 'text'],
},
}, { orientation: true });
anatomies (@sigx/zero/anatomy) is the closed registry of all 51 scopes, declared
as const, so ZeroScope is a closed literal union. That closure is load-bearing: a
generated /register artifact asserts its scope keys against it at compile time, which is
what makes a typo'd or version-skewed scope a compile error rather than a silent fall-through
to the open unions. anatomy.selector(part, state?) builds the CSS selector for a part;
anatomy.toJSON() emits exactly the shape the manifest carries.
The manifest
@sigx/zero/manifest.json is the same registry as JSON: zeroVersion, the
attributeSpec (attribute names, the flag form, the flag / state / placement vocabularies,
the synonym table, the named variant axes and the open data-<axis> form), the token grammar
(colors, categories, the recommended ramps), and components — one toJSON() snapshot
per scope, each part with its parent, states, flags, placements, hiddenIn,
pseudo, token hints and ready-made per-state selector fragments. The recipe compiler,
the validator and the generation skill all consume it. See
Manifests and schemas.
Enforcement
expectAnatomy(container, anatomy) from @sigx/zero/testing is the assertion zero's own
suite runs against every rendered part, and the one an ecosystem component is held to:
declared parts only, states from the closed set, flags declared and presence-only,
data-placement from the part's declared subset, DOM nesting matching the part tree, and
hidden exactly where hiddenIn says. It throws a plain Error, so it works under any test
runner. Custom axes are passed in explicitly: expectAnatomy(el, anatomy, { axes: ['emphasis'] }).
expectAnatomyElements runs the same rules over an ElementLike
(getAttribute / getAttributeNames / parent), so a non-DOM renderer holds its components
to the identical contract. See Building your own component.
