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#

HTML
<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-scope is the component name, kebab-case (tabs, radio-group, tree-view).
  • data-part is the part name, kebab-case, from a closed per-component list.
  • data-state holds exactly one value at a time from a closed, per-part set.
  • Flags are boolean and presence-only: data-disabled="", never data-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:

FamilyValues
presenceopen | closed
selectionchecked | unchecked | indeterminate
activationactive | inactive
toggleon | off
loadingloading | loaded | complete | error
fillfull | 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:

TypeScript
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.