Typed vocabulary
By default the variant-axis props are open unions: any design-system-declared
value is valid, and the recommended names autocomplete. A design system's generated
/register module narrows them — one side-effect import, and every component's color,
size, variant, axes and mods props accept exactly what that design system's
compiled CSS answers to, per component. No import, no change.
Opting in
// app entry — once
import '@sigx/zero-basic/register';
<Button.Root variant="outline">…</Button.Root> // ok — basic wires it on button
<Button.Root variant="dash">…</Button.Root> // error — not in basic's button vocabulary
<Tabs.Root variant="outline">…</Tabs.Root> // error — basic wires no variant on tabs
<Badge.Root variant="ghost">…</Badge.Root> // error — badge's own set is solid | soft | outline
What narrows:
- Per-component axis props. Each of
color/size/variantbecomes the literal union the design system's recipes wire for that scope;axesandmodsbecome the declared bags. An axis the design system leaves unwired on a scope is typednever— and since sigx's JSX prop surface stripsnever-valued props, the prop simply does not exist on that component. The generated file carries a doc comment on eachnevernaming the reason (the scope declared it empty, the design system has no such axis, or no recipe wires it). - Theme names.
setTheme()andThemeProvider'sthemeprop take the design system's real theme names;setTheme('dimm')is an error. Reads stay open (see Theming). - Custom properties, breakpoints and token keys.
cssVar(name)autocompletes every custom property the compiledtokens.cssemits;token(category, key)autocompletes the keys of each token category;ZeroBreakpointis the declared breakpoint names.
import { cssVar, token } from '@sigx/zero';
const ring = cssVar('--color-primary'); // 'var(--color-primary)'
const gap = token('space', 'md'); // 'var(--space-md)'
How it works
@sigx/zero exports one empty interface:
export interface ZeroVocabulary {}
The variant-axis prop types are generic on the component scope
(WithVariantAxes<'button'>) and resolve through it: ColorValueFor<S>, SizeScaleFor<S>,
VariantValueFor<S>, AxesFor<S>, ModsFor<S>. With nothing augmenting the interface they
fall back to the open unions. A design system's register.d.ts is a declare module '@sigx/zero' block that augments ZeroVocabulary with theme, breakpoint, property,
tokens and components — one entry per compiled scope, each carrying all five members.
The resolvers keep three cases distinguishable: no augmentation → the open fallback;
declared → the literal union; declared empty → never. That is why roles: {} in a design
system (no colour axis) removes the color prop everywhere rather than leaving it open.
The artifact is generated, never authored
sigx zero:build writes dist/register.d.ts and an empty dist/register.js for every
design system, from the harvest — what the recipes actually wire — rather than the
declaration. The harvest is strictly stronger: it refuses to type a value the compiled CSS
does not implement. The file ends with two self-verifying assertions that make it fail its
own compilation rather than silently degrade: every components key must be a ZeroScope
(a typo'd or version-skewed scope would otherwise take the open fallback and un-narrow
exactly the component it meant to narrow — merged ecosystem scopes are excluded by name),
and every entry must carry all five members (a truncated entry would silently un-narrow the
axis it omitted).
A design system package exposes it as the ./register subpath:
"./register": { "types": "./dist/register.d.ts", "import": "./dist/register.js" }
One register per program
Augmentation is program-wide: importing two design systems' /register modules in one
program merges two augmentations of the same interface and produces nonsense. An app uses
one design system's register. A program that needs vendor-named props for a design system —
or two design systems' typed surfaces side by side — uses the ./components path instead,
which is self-contained and augments nothing. See
Vendor-named component APIs.
For ecosystem components
WithVariantAxes<S> constrains S to zero's own closed scope registry, so a typo'd scope
literal is a compile error rather than a silently different type. An out-of-tree component
uses WithVariantAxesOpen<S extends string> — the open constraint is the deliberate cost of
a scope the registry cannot know, and a register module built for the design system is what
closes the loop. See Building your own component.
