statecraft.yaml

The design-system manifest. The import agent writes it for you, so treat this as the page you come to when something specific is wrong — not as something to read start to finish. For what to do about a broken import, start at Design systems.

The file and the row are two documents

This trips up everyone once. A kind: live manifest carries two kinds of field, and they go to different places.

  • Editor-facing fieldsname, source, scope, components, typography, tokens, providers, peers, stylesheets, fonts — describe what the canvas renders. These are mirrored onto your workspace's design-system row on every successful build.
  • Build-only blocksbuild, install, tailwind.build, cssEngine, pandaConfig, unocss, resolveConditions, aliases, scssShared, defines — describe how to compile the bundle. These are stripped before the body reaches the row.

So the YAML you see in the app is a subset of the file in your repo. That is why the in-app editor is read-only for manifest-managed rows: it couldn't round-trip the build config even if it wanted to. Edit the file.

tailwind has two sibling sub-blocks and only one is stripped. tailwind.build (mode, globals, config path) is build-time. tailwind.render (an inline config object for the preview iframe's Tailwind CDN) is row-side and survives. vueVersion is the other shared key — it pins the compiler and the iframe runtime together.

source

Exactly one of five kinds. Everything else in the file depends on this choice. See Design systems for which to pick.

source:
  live: true              # your repo, compiled by Statecraft (canonical for real teams)

source:
  npm: "antd"             # a published package, from esm.sh
  version: "5.22.0"       # exact semver — ranges and "latest" are rejected
  deps:                   # optional: pin transitive peers esm.sh resolves wrong
    "@mantine/hooks": "7.14.1"

source:
  css: true               # a stylesheet; components synthesised from cssMapping

source:
  kind: none              # plain HTML tags

source:
  kind: host              # a pre-built ESM bundle you host

Version pins for npm must be exact — a range would let a compromised upstream propagate into your canvas automatically. Subpaths go directly in npm (e.g. @mui/material/styles).

For css, each component entry declares a cssMapping and Statecraft synthesises a wrapper that renders the mapped tag and merges variant class names:

components:
  - label: Button
    tagName: Button
    snippet: '<Button variant="primary">Click</Button>'
    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

Deliberately simple — no ref forwarding, no internal state. Anything that needs behaviour (Tooltip, Dropdown) wants a real component library.

build

What the bundle is. entry and framework are required when the block is present, and paths resolve relative to the manifest.

build:
  entry: ./src/index.ts        # bundle entry
  framework: react             # react | vue — must match the row
  frame: ./src/Frame.tsx       # optional provider wrapper (a file in your repo)

  # OR, when no such file exists, inline it — relative imports resolve
  # against the REPO ROOT, not the manifest:
  # frameSource: |
  #   import "./src/styles/globals.css";
  #   export default ({ children }) => <ThemeProvider>{children}</ThemeProvider>;
  # frameLanguage: tsx         # tsx | jsx | vue (defaults from framework)

The frame is how you reproduce your app shell — the global stylesheet, the theme provider, the CSS reset that your components silently depend on. It is the fix for "everything renders but unstyled". frame and frameSource are mutually exclusive.

install

Where and how to install dependencies before bundling.

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

skipInstall is what you want in CI, where your workflow has already installed. packageManager: pnpm is the escape from the npm 11 arborist crash on Node 22+.

tailwind

tailwind:
  build:                          # build-time (stripped from the row)
    mode: v4                      # v3 | v4 | none
    globalsCss:
      - ./src/styles/globals.css  # required when mode is set
    config: ./tailwind.config.ts  # REQUIRED for v3 — there is no fallback
    content: ["./src/**/*.tsx"]   # extra template globs to scan

  render:                         # runtime (survives onto the row)
    enabled: true
    config: { theme: { extend: {} } }

None of this is detected for you. Tailwind setup is explicit configuration — if the mode is set and globalsCss is missing, or v3 is set without config, the build fails rather than guessing.

cssEngine

Declare your CSS-in-JS engine and the bundler installs the matching Vite / Babel / PostCSS plugin. Three families behave differently:

FamilyEnginesNotes
Compile-timevanilla-extract, Linaria, Pigment CSS, StyleX, Compiled, Macaron, Windi, PandaEmit real CSS at build. Panda additionally needs pandaConfig and a cssEngineImports entry pointing at your @layer CSS file — without it the bundle ships 0 KB of CSS.
RuntimeEmotion, styled-components, Stitches, Goober, JSS, Theme UI, FelaShip as normal npm libraries and inject styles at render. Declaring the engine tells Statecraft a 0 KB CSS bundle is expected, not a failure.
UnoCSSUnoCSSHas its own block, not a cssEngine value.
cssEngine: vanilla-extract

# Panda needs both of these:
# pandaConfig: ./panda.config.ts
# cssEngineImports: [./src/styles/index.css]

# UnoCSS rides its own block:
# unocss:
#   build:
#     config: uno.config.ts
#     presets: [uno]

# Vite passthrough:
# aliases:
#   - { name: "@", path: src }
# resolveConditions: [source]
# scssShared: ./src/styles/_shared.scss
# defines:
#   - { key: __DEV__, value: "false" }

scope

Which exports are legal as JSX tags. Hand-authored — Statecraft never writes this for you, because guessing would expose hooks and cva() helpers as broken pseudo-components. The real export list is written to _design-systems/<slug>.exports.txt for you to copy from.

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

# Or expose a whole module under one name:
scopeNamespace: antd       # JSX: <antd.Button>, <antd.Card>

The two can coexist. Remember that scope alone gives you a working design system with an empty palette — you also need a components entry for anything you want to drag out.

components

The palette. snippet is the JSX inserted when you place one; props tells the Properties inspector what editors to show.

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
        defaultValue: default
        options:
          - { label: Default, value: default }
          - { label: Ghost, value: ghost }
      - name: disabled
        type: boolean
        label: Disabled
typeEditor
stringText input, or a dropdown when options is given.
numberNumeric input.
booleanCheckbox.
expressionFree-form JSX expression — onClick handlers and the like.
colorColour picker plus the colours-token dropdown.
tokenPaired with tokenKind (e.g. spacing), surfaces that token category as a dropdown.
iconPaired with iconSource (a scope namespace), renders an icon picker.

responsive: true on any prop wraps it in a breakpoint tab strip.

typography

Your type ramp. Declaring it drives the canvas Text tool's style dropdown and the inspector's text-style switcher; leave it out and both fall back to plain HTML roles. Each snippet is one root element whose text is Text.

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

tokens

Each category is a flat map. The inspector offers them as quick-picks on the matching CSS property, and they're emitted as CSS custom properties in the preview iframe, so snippets can reach them via var(--color-brand). Key casing is preserved — textFaint becomes var(--color-textFaint).

tokens:
  colors:
    brand: "#3245ff"
  colorsDark:             # optional parallel override for dark mode
    brand: "#7a8fff"
  spacing:
    md: "16px"
  radii:
    pill: "999px"

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

providers

The root wrapper chain, outermost first. Each wraps the next; the last wraps your canvas content.

providers:
  - import: ThemeProvider
    props:
      theme:
        $factory: createTheme        # call a factory from the module
        $args:
          - palette: { mode: light }
    siblings:
      - import: CssBaseline          # side-effect component, not a wrapper
  - import: ToastProvider
    props: {}

$ref resolves a dotted path to a module export ($ref: theme.darkAlgorithm). siblings are for components that must live inside the provider but don't compose as wrappers — MUI's <CssBaseline />, Chakra's <ColorModeScript />.

Related: styleCache (emotion, antd-cssinjs, griffel, styled-components, styletron) is an optional hint that shares one engine instance across your library and its additionalModules. You usually don't need it — each design system already renders in its own iframe, so its styles can't leak.

peers

If your bundle bare-imports something Statecraft doesn't pin, the import fails at load. Declare it here to append an importmap entry.

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

Eleven specifiers are locked and will fail validation: react, react-dom, react-dom/client, scheduler, and the routing primitives (react-router-dom, wouter, wouter/memory-location, @tanstack/react-router, next/link, next/navigation, next/router). These must stay singletons with the host — selection, inline editing and the canvas's no-op router all depend on both sides resolving the same module instance.

A peer the host already pins can't be overridden — browser importmap rules don't allow it, and the bundle resolves to the host's version. The dev console logs a warning so the mismatch is at least visible.

For icon packs and companion libraries, use additionalModules instead — each gets its own version pin, scope list and optional namespace:

additionalModules:
  - npm: "@radix-ui/react-icons"
    version: "1.3.2"
    scope: [HeartIcon, StarIcon]
    scopeNamespace: Icons      # JSX: <Icons.HeartIcon />

Styling & fonts

These feed the preview iframe directly and work with every source kind.

stylesheets:                # <link rel="stylesheet"> in each iframe head
  - "https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"

globalCss: |                # raw CSS injected into each iframe head
  body { margin: 0 }

fonts:
  - family: Inter
    url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600"

themeModes: [light, dark]   # what the canvas theme toggle offers