Reference

YAML schema reference

Every design system is described by a YAML document. The editor surfaces parse errors inline and the canvas shows resolve errors when a component can't load. This reference covers every section of the schema with examples.

Who this is for

Most designers never need to read this page. The desktop tray's Add a design system… wizard, the browser importer, and the editor's Design Systems panel author this YAML for you — for the common case, you point Statecraft at your repo and it writes the manifest itself.

The reasons you might end up here:

  • You're hand-authoring a design system from scratch (rare — the importer is usually faster).
  • You're debugging why a component won't render and want to check what the manifest is actually saying.
  • You're wiring up something the wizard doesn't cover — a custom provider chain, an unusual CSS engine, an externalised dependency, a per-DS Tailwind config.
  • You're forking a Core gallery starter (Ant Design, MUI, shadcn) and want to know what every field does before you edit.

If you're a designer pointed here by a teammate, the Design Systems panel in the editor is the friendlier surface for most edits — this reference is the long form for when that panel doesn't expose what you need.

Two views of one schema. For most design systems the YAML lives on the workspace and the editor is where you change it. A source.live design system instead reads from a statecraft.yaml manifest committed in your repo: it carries everything below plus a set of CI-only build blocks (build, install, tailwind.build, cssEngine, …) that tell the daemon and statecraft publish how to compile the bundle. Those blocks are stripped before the body is mirrored onto the workspace row, so the editor view is read-only for live rows and shows the stripped remainder. See the build config section below.

The Core gallery starters are editable YAML examples of common patterns. Ant Design covers esm.sh externals, MUI covers the emotion style cache, shadcn covers a same-origin host bundle. Install any of them via Browse gallery on the Design Systems page, then open the row to read the YAML directly.

File at a glance

name: Acme Components
source:
  npm: "@acme/react"
  version: "2.1.0"

scope:
  - Button
  - Card
  - Input
  - default as AcmeLogo

additionalModules:
  - npm: "@acme/icons"
    version: "1.4.0"
    scope: [SearchIcon, CloseIcon]
    scopeNamespace: Icons

providers:
  - import: AcmeProvider
    props:
      theme: { mode: light }

styleCache:
  kind: emotion

tokens:
  colors:
    brand: "#3245ff"
    ink: "#121316"
  spacing:
    xs: "4px"
    sm: "8px"
    md: "16px"

components:
  - label: Button
    tagName: Button
    category: Form
    snippet: "<Button>Click me</Button>"
    props:
      - name: variant
        type: string
        label: Variant
        options:
          - { label: Solid, value: solid }
          - { label: Ghost, value: ghost }

typography:
  - label: Heading 1
    snippet: '<Heading size="7">Text</Heading>'
  - label: Body
    snippet: '<Text size="3">Text</Text>'

source: where components come from

The source block picks exactly one of five loading strategies. Every other field in the file depends on this choice.

source.npm

Load from esm.sh. The package must be ESM-compatible. The version must be an exact semver; ranges like ^1.2.3 or latest are rejected so that a compromised upstream cannot propagate automatically.

source:
  npm: "antd"
  version: "5.22.0"
  # Optional: pin transitive peers esm.sh might resolve wrong
  deps:
    "@mantine/hooks": "7.14.1"
  # Optional: list packages to emit as bare imports so the host
  # importmap catches them (needed for CJS→ESM named-export workarounds)
  external:
    - "@ant-design/colors"

Subpaths go directly in npm, e.g. @mui/material/styles. The resolver splits the package name from the subpath internally.

source.css

No module is loaded. Each entry in components that declares a cssMapping is synthesised by the resolver into a tiny wrapper component (React or Vue, matching the design system's framework): it renders the mapped HTML tag, merges defaultClass + variant classes + any user className, and spreads remaining props as HTML attributes. Styling comes from stylesheets / globalCss / tailwind, same as every other source. Good for designers onboarding a plain CSS kit (Bulma, Bootstrap, BEM) without a component library behind it.

source:
  css: true

stylesheets:
  - "https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"

components:
  - label: Button
    tagName: Button
    category: Components
    snippet: '<Button variant="primary">Click</Button>'
    props:
      - name: variant
        type: string
        label: Variant
        options:
          - { label: Primary, value: primary }
          - { label: Danger, value: danger }
    cssMapping:
      tag: button
      defaultClass: button
      variants:
        variant:                      # enum-style: value -> class
          primary: is-primary
          danger: is-danger
        # dismissible: is-dismissible # boolean-style: truthy prop -> class
      # slots:                        # compound sub-components
      #   - name: Header
      #     tag: header
      #     className: card-header

This source kind is deliberately simple — no ref forwarding, no internal state, no compound components beyond static slot wrappers. If a component needs behaviour (Tooltip, Dropdown), use an additionalModules entry alongside your CSS-first primitives or step up to a real React library.

source.none

No module loaded. Used with scope populated by native HTML tags (div, button, input, form, and so on). Suitable for prototypes that do not require a branded component library.

source.live

A continuously-rebuilt bundle produced from a local repo — by the desktop app's daemon on every save (~2 seconds end-to-end), or by statecraft publish from CI. The workspace row carries no path information; the bundle pointer lives on the row and is rotated on each successful build, so collaborators keep rendering the last-published bundle even with the daemon offline.

name: Acme UI
source:
  live: true        # canonical spelling; `local: true` is a legacy alias

scope:
  - Button
  - Card
  - Badge

Register the repo once via the desktop app's Add a design system… wizard or statecraft import. That writes a committed statecraft.yaml manifest in your repo plus a thin, gitignored pointer to it at <repo>/.statecraft/live.json (just { workspaceId, slug, manifestPath, createdAt }). The manifest — not the sidecar — is the single source of truth for the build: it carries the entry, framework, frame wrapper, install config, and Tailwind / CSS-engine setup alongside the editor-facing fields (scope, components, tokens, …). See the build config section for those blocks, or Import your design system for the full setup walkthrough.

Edit the manifest, not a CLI flag. There is no statecraft live update verb — to change install, Tailwind, CSS-engine, frame, or scope config, edit the committed statecraft.yaml and save. The daemon rebuilds on the file-watch; for CI, commit and let statecraft publish read the same file. The daemon also mirrors the manifest's editor-facing fields onto the workspace row on every build, so edits to scope: / components: reach the canvas palette — which is why the in-app YAML editor is read-only for live rows.

source.host

A same-origin bundle shipped by the Statecraft app itself. The shadcn/ui gallery starter uses this path: the bundle JS+CSS are served from /builtin-bundles/ and registered in src/adapters/hostModules.tsx. New host entries are intentionally rare — bring-your-own design systems use source.live (via statecraft import) instead.

Build config: the statecraft.yaml manifest

Everything else on this page describes the YAML stored on a workspace row. A source.live design system adds a second layer: the statecraft.yaml manifest committed in your repo, read identically by the desktop daemon on every rebuild and by statecraft publish from CI. The manifest is a superset of the row schema — the same editor-facing fields plus the CI-only blocks below that describe how to compile the bundle. Those blocks are stripped before the body is mirrored onto the row, so the editor never acts on them.

# packages/ui/statecraft.yaml
name: Acme UI
source:
  live: true

# --- editor-facing fields (mirrored onto the workspace row) ---
scope:
  - Button
  - Card
components:
  - label: Button
    tagName: Button
    category: Actions
    snippet: "<Button>Click</Button>"

# --- CI-only build blocks (stripped before the row is stored) ---
build:
  entry: ./src/index.ts        # bundle entry, relative to the manifest
  framework: react             # react | vue — must match the row
  frame: ./src/Frame.tsx       # optional provider wrapper (a repo file)
  # frameSource: |             # OR inline the wrapper when no file exists
  #   import "./src/styles/globals.css";
  #   export default ({ children }) => <ThemeProvider>{children}</ThemeProvider>;
  # frameLanguage: tsx         # tsx | jsx | vue (defaults from framework)

install:
  installCwd: ..               # run install at the workspace root (monorepos)
  packageManager: pnpm         # npm | pnpm | yarn | bun
  # installCommand: "pnpm install --filter ui..."
  # skipInstall: true          # reuse an existing node_modules (CI)
  # runInstallScripts: true    # allow dependency postinstall scripts
  # workspaceBuildCommand: "pnpm -F ui build"  # run before the bundle build

tailwind:
  build:                       # build-time — sibling of runtime tailwind.render
    mode: v4                   # v3 | v4 | none
    globalsCss:
      - ./src/styles/globals.css
    # config: ./tailwind.config.ts   # required when mode: v3
    # content: ["./src/**/*.tsx"]    # extra template globs to scan

cssEngine: vanilla-extract     # build-time CSS-in-JS engine
# pandaConfig: ./panda.config.ts     # required when cssEngine: panda
# aliases:
#   - { name: "@", path: src }
# resolveConditions: [source]
# scssShared: ./src/styles/_shared.scss
# defines:
#   - { key: __DEV__, value: "false" }

build

Identifies what the bundle is. The build block is optional, but when present entry and framework (react or vue, must match the row) are both required and resolve relative to the manifest file. frame points at a provider-wrapper module the bundle re-exports as frameComponent so the canvas wraps every render in it; frameSource (with optional frameLanguage) inlines that wrapper when no single module exists to point at — mutually exclusive with frame, and relative imports resolve against the repo root.

install, engines, and Vite settings

The remaining CI-only fields tell the bundler how to build. install covers monorepo install location and package manager (installCwd, packageManager, installCommand, skipInstall — this is where you switch to pnpm if the npm 11 arborist bug bites — plus runInstallScripts and workspaceBuildCommand for repos that need dependency postinstall scripts or a workspace build before the bundle compiles); tailwind.build wires Tailwind v3/v4; cssEngine (plus pandaConfig for Panda) selects a build-time CSS-in-JS engine; aliases, resolveConditions, scssShared, and defines map onto the matching Vite settings. UnoCSS rides its own unocss.build: block — read by the daemon and statecraft publish, not by the editor schema (the in-app validator ignores it). The full field-by-field reference, the CSS-engine catalogue, and the GitHub Actions workflow live in Keep components in sync.

tailwind has two sibling sub-blocks: tailwind.build (build-time — mode / globalsCss / config as a path / content globs) is CI-only and stripped; tailwind.render (runtime — enabled / config as an inline object / customCss) is row-side and survives onto the editor view for the preview iframe's Tailwind Play CDN.

scope: which exports become JSX tags

Every name in scope is a module export that becomes available as a JSX tag in snippets. Two aliasing patterns are supported:

scope:
  - Button                 # import Button, use as <Button>
  - default as AcmeLogo    # import default, use as <AcmeLogo>
  - Card as Panel          # import Card, use as <Panel>

For libraries with many exports, scopeNamespace exposes the whole module under one name:

scope: []
scopeNamespace: antd     # JSX: <antd.Button>, <antd.Card>

The two can coexist — list specific exports in scope for the common ones and access the rest through the namespace.

peers: bring-your-own importmap entries

A design-system bundle whose bare imports (e.g. import "@tanstack/react-query") reference a peer Statecraft doesn't pin in its host importmap won't resolve — the import fails at load time. Declare those peers here to append an importmap entry before the bundle loads.

peers:
  - specifier: "@tanstack/react-query"
    url: "https://esm.sh/@tanstack/react-query@5.64.0?external=react,react-dom"
  - specifier: "zustand"
    url: "https://esm.sh/zustand@5.0.2?external=react"

Two rules to keep in mind:

  • Locked host peers are rejected. react, react-dom, react-dom/client, and scheduler must stay singletons with the host — the portal bridge that makes element selection and inline editing work depends on it. The routing primitives are locked for the same reason: react-router-dom, wouter, wouter/memory-location, @tanstack/react-router, and the Next shims next/link, next/navigation, next/router — the canvas mounts a no-op router/shim above your JSX, and its context only reaches your components if both sides resolve the same module instance the host pins. Declaring any of these eleven specifiers in peers fails validation.
  • Shadowed pins log a warning. If a peer specifier is already pinned by the host (e.g. styled-components is host-pinned to v5), declaring a different URL won't override it — the bundle resolves to the host version. Multi-importmap browser rules don't permit override. The dev console logs a warning so the mismatch is legible.

additionalModules: secondary npm packages

For icon packs, subpath entry points, or companion libraries. Each module gets its own version pin, scope list, and optional namespace.

additionalModules:
  - npm: "@radix-ui/react-icons"
    version: "1.3.2"
    scope: [HeartIcon, StarIcon, ChevronDownIcon]
    scopeNamespace: Icons

With the namespace set, JSX references look like <Icons.HeartIcon />. Without it, icons live in the top-level scope.

providers: the root-level wrapper chain

Many libraries need one or more providers at the root (ThemeProvider, ConfigProvider, ToastProvider). Providers are listed outermost-first; each wraps the next, and the last one wraps the canvas content.

providers:
  - import: ThemeProvider
    props:
      theme:
        $factory: createTheme
        $args:
          - palette: { mode: light }
    siblings:
      - import: CssBaseline
  - import: ToastProvider
    props: {}

$factory calls a factory function from the module; $ref resolves a dotted path to a module export ($ref: theme.darkAlgorithm). siblings are side-effect components that must live inside the provider but do not compose as wrappers — MUI's <CssBaseline />, Chakra's <ColorModeScript />.

styleCache: optional CSS-in-JS engine hint

Each design system renders inside its own sandboxed iframe, so a library's runtime styles land in that frame's own document — they render correctly and can never touch Statecraft's own UI. You usually don't need styleCache at all.

Declaring it is an optional optimisation: it shares a single instance of the CSS-in-JS engine across your library and any additionalModules (so, for example, a component package and a companion data-grid package resolve the same theme and cache). Omit it and the engine is simply bundled in per module.

# Emotion-based libraries (Chakra, MUI, Mantine, Theme UI, Joy UI)
styleCache:
  kind: emotion

# Ant Design's cssinjs engine
styleCache:
  kind: antd-cssinjs

# Fluent UI (Griffel)
styleCache:
  kind: griffel

# styled-components (Grommet, Primer, styled-ecosystem libraries)
styleCache:
  kind: styled-components

# Styletron (Base Web)
styleCache:
  kind: styletron

tokens: typed design tokens

Every token category is a flat Record<string, string>. The Properties inspector uses them as quick-pick dropdowns on the matching CSS property; the resolver emits them as CSS custom properties in the preview iframe so snippets can reach them via var(--color-brand) and similar.

tokens:
  colors:
    brand: "#3245ff"
    ink: "#121316"
  colorsDark:             # optional parallel override for dark mode
    brand: "#7a8fff"
    ink: "#efe9dc"
  spacing:
    xs: "4px"
    sm: "8px"
    md: "16px"
    lg: "24px"
  radii:
    sm: "4px"
    md: "8px"
    pill: "999px"
  fontSizes:
    sm: "13px"
    base: "14px"
    lg: "17px"

Supported categories: colors, colorsDark, spacing, radii, fontFamilies, fontSizes, fontWeights, lineHeights, letterSpacing, shadows, borderWidths, breakpoints, zIndex, opacity, durations, easings.

components: the insert menu

Each entry becomes a row in the component picker on the canvas. The snippet is the JSX inserted on click; the props list tells the Properties inspector how to render editors for each attribute.

components:
  - label: Button
    tagName: Button
    category: Form
    description: Primary action trigger
    docsUrl: "https://ui.shadcn.com/docs/components/button"
    snippet: "<Button>Click me</Button>"
    props:
      - name: variant
        type: string
        label: Variant
        group: appearance
        options:
          - { label: Default, value: default }
          - { label: Ghost, value: ghost }
          - { label: Destructive, value: destructive }
        defaultValue: default
      - name: size
        type: string
        label: Size
        options:
          - { label: Small, value: sm }
          - { label: Default, value: default }
          - { label: Large, value: lg }
      - name: disabled
        type: boolean
        label: Disabled

Prop types

  • string — text input, with optional options for a dropdown.
  • number — numeric input.
  • boolean — checkbox.
  • expression — free-form JSX expression, used for onClick handlers and similar.
  • color — native color picker plus the colors-token dropdown.
  • token — paired with tokenKind (e.g. spacing, fontSize), surfaces the matching token category as a dropdown.
  • icon — paired with iconSource (a scope namespace), renders a visual icon picker.

Setting responsive: true wraps any prop in a breakpoint tab strip. The editor serialises single-value entries as scalars and multi-value entries as object literals keyed by breakpoint name.

typography: text styles for the Text tool

Declare your type ramp here and it drives the canvas Text tool's style dropdown and the Properties inspector's text-style switcher. Each entry is a label plus a snippet — one root element whose text content is Text. The snippet is whatever your system styles text with: a component, a prop preset of one, or class-named HTML.

typography:
  - label: Display
    snippet: '<Heading size="9">Text</Heading>'
  - label: Heading 1
    snippet: '<Heading size="7">Text</Heading>'
  - label: Body
    snippet: '<Text size="3">Text</Text>'
  - label: Caption
    snippet: '<Text size="1" color="gray">Text</Text>'
  - label: Eyebrow                 # class-based ramps work too
    snippet: '<p className="eyebrow">Text</p>'

Leave it out and the Text tool falls back to plain HTML roles (Heading 1–6, Paragraph, Caption, …). Picking a style inserts its snippet; selecting a text element and choosing another style re-styles it in place, keeping its text.

Styling, fonts & theming

Several top-level fields feed the preview iframe directly. They apply to every source kind.

  • stylesheets — array of URLs loaded as <link rel="stylesheet"> in each iframe's head. Absolute, root-relative (/builtin-bundles/…), or the host:<name> scheme.
  • globalCss — a raw CSS string injected into each iframe's head.
  • fonts — array of { family, url, weight?, style?, display? }. A stylesheet URL renders as a <link>; a direct font file is wrapped in an @font-face rule.
  • assets — passthrough { name, url } pairs exposed in JSX scope as assets.<name> (e.g. <img src={assets.logo} />). Names must be valid JS identifiers.
  • shadcnTheme — typed shortcut for the shadcn CSS-variable contract: light vars emit under :root, dark under [data-theme="dark"]. Strictly additive to globalCss.
  • themeModes — list of light / dark. Listing both enables the host's light/dark toggle and makes { $mode: { light, dark } } markers in provider props resolve reactively.
  • tailwind.render — runtime Tailwind for the iframe's Tailwind Play CDN: enabled, an inline config object, and customCss. (The build-time tailwind.build sibling is covered under build config.)
  • fallbackProps — a props schema applied to tags not listed in components, so the Properties panel can still offer className / style / children editors.

Common esm.sh errors

Most setup friction comes from esm.sh's CJS→ESM transform. The errors below cover the common cases.

"doesn't provide an export named: 'X'"

The package (or a transitive dep) uses CJS Object.defineProperty re-exports that esm.sh's static analyzer cannot see. Fix: add the owning package to source.external, then ask the Statecraft maintainer to add an importmap entry pointing at https://esm.sh/<pkg>@<version>?exports=X&external=react,react-dom. The Ant Design gallery starter is a working three-package example of this pattern.

"Named export(s) not found in X"

The name in scope is not an export of the package. If it only exists on the default export, alias with "default as X". Spellings can be verified against the package's index.d.ts.

404 on an esm.sh URL

Usually a bad source.version. If a transitive peer is the cause, pin it under source.deps.

Preview renders unstyled

Almost always a missing styling context, not a styleCache issue. Check, by source kind:

  • CSS / stylesheet kits — the stylesheet didn't load. Confirm the URL under stylesheets (or the CSS in globalCss) is correct and reachable.
  • Component libraries — the theme provider or CSS reset is missing. Declare it in providers (for source.npm), or add a frame wrapper via build.frame / build.frameSource (for source.live) so components render in the same context they would in production.

Runtime CSS-in-JS libraries (emotion, styled-components, …) render styled with no extra setup: each state is its own sandboxed iframe, so their injected <style> tags land in that frame's document. styleCache is not required to fix unstyled output — it's only the shared-instance optimisation described above.

More examples

The Core gallery starters install as editable YAML you can fork. Ant Design covers externals and cssinjs, MUI covers emotion and CssBaseline, and shadcn covers the host source.