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-headerThis 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, andschedulermust 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 shimsnext/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 inpeersfails validation. - Shadowed pins log a warning. If a peer specifier is already pinned by the host (e.g.
styled-componentsis 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: IconsWith 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: DisabledProp types
string— text input, with optionaloptionsfor a dropdown.number— numeric input.boolean— checkbox.expression— free-form JSX expression, used foronClickhandlers and similar.color— native color picker plus the colors-token dropdown.token— paired withtokenKind(e.g.spacing,fontSize), surfaces the matching token category as a dropdown.icon— paired withiconSource(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 thehost:<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-facerule.assets— passthrough{ name, url }pairs exposed in JSX scope asassets.<name>(e.g.<img src={assets.logo} />). Names must be valid JS identifiers.shadcnTheme— typed shortcut for the shadcn CSS-variable contract:lightvars emit under:root,darkunder[data-theme="dark"]. Strictly additive toglobalCss.themeModes— list oflight/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 inlineconfigobject, andcustomCss. (The build-timetailwind.buildsibling is covered under build config.)fallbackProps— apropsschema applied to tags not listed incomponents, so the Properties panel can still offerclassName/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 inglobalCss) is correct and reachable. - Component libraries — the theme provider or CSS reset is missing. Declare it in
providers(forsource.npm), or add a frame wrapper viabuild.frame/build.frameSource(forsource.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.