Component specs
A component spec is a small, platform-independent contract for one component:
its variant axes and each axis's option names (variant:
Primary/Secondary/Outline/…, size: sm/md/lg/…), and its
parts — the component and every sub-component it exports — with the props
each one declares itself. Not the Tailwind classes an option resolves to
(that's implementation), just the shape of the API surface every platform
port is expected to match.
{
"$schema": "https://kinetixui.com/schema/component-spec.json",
"name": "button",
"title": "Button",
"status": "stable",
"since": "0.4.1",
"platforms": ["React", "SwiftUI", "Compose", "Flutter"],
"variants": {
"variant": ["Primary", "Secondary", "Outline", "Destructive", "Ghost", "Link"],
"size": ["sm", "md", "lg", "icon"],
"state": ["Default", "Hover", "Focus", "Active", "Disabled"],
"corners": ["sharp", "default", "pill"]
},
"components": [
{
"name": "Button",
"props": [
{
"name": "asChild",
"type": "boolean",
"required": false,
"description": "render as the single child element (Radix Slot) instead of <button>"
},
{ "name": "variant", "type": "\"Primary\" | \"Secondary\" | … | null", "required": false }
]
}
],
"source": "packages/ui/src/components/button.tsx"
}status, since and platforms are not extracted from source — they come from
components.manifest.json, the single place per-component lifecycle and platform
coverage are edited (see Contributing). since is the first
tagged release that contained the component.
Generated, not hand-written#
scripts/gen-component-specs.mjs, run as part of pnpm build:registry, builds
every spec from the source itself, the same way the token CSS and the registry
JSON are generated rather than maintained by hand — a hand-written manifest
drifts from the real component within a few PRs:
variantscome from the component's owncva()(class-variance-authority) definition. Components without one get{}.componentscome from the TypeScript compiler: every exported PascalCase callable, with the props its type declares. Props inherited from the HTML element or Radix primitive it wraps are left out, so a part with an emptypropslist simply accepts that element's own props (CardHeaderis adiv).requiredand the JSDocdescriptionare carried over.
CI's "generated files in sync" gate covers specs/components/** too, so a
component's API can't change without its spec changing in the same commit.
Coverage#
Every registry component has a spec. avatar-group (shipped inside avatar) and
combobox (a documented composition of Popover and Command) have no file of
their own; index.json lists them under companions, and its coverage
({ covered, total }) counts registry components only.
What a spec does not capture yet: inherited element props, event and slot semantics, keyboard behaviour, and per-platform API differences. Those are follow-ups the manifest is the natural home for.
Where it's served, and who reads it#
Every generated file is served statically at kinetixui.com/specs/<name>.json
(a sibling of the registry, kinetixui.com/r/<name>.json) plus a
flat kinetixui.com/specs/index.json. The CLI's inspect command
reads it — kinetixui inspect button prints the first release and status, the
variant axes, and each part with the props it declares (? marks optional)
alongside the registry metadata it already showed. Nothing else consumes it yet;
the doc that originally called for this (design-system contract manifests
powering docs/Figma/codegen/testing/changelogs) treats it as a foundation other
tooling builds on over time, not a one-shot deliverable.