Skip to content
Kinetixui

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:

  • variants come from the component's own cva() (class-variance-authority) definition. Components without one get {}.
  • components come 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 empty props list simply accepts that element's own props (CardHeader is a div). required and the JSDoc description are 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.