sb-health — design-system health gate

SkillMedia

Check design-system health for a React+Vite app, raw hex, undefined/unused tokens, scale gaps, DESIGN.md drift, and (opt-in) property→token-family misuse. Use for 'is my design system healthy', 'find raw colors', 'check tokens', or a mixed design system.

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the sb-health skill

What this skill tells your AI

The instructions your AI receives, as published by strongeron/storybook-workbench in skills/sb-health/SKILL.md and read by ahel’s review.

The browser can't shell out, so the script runs the checks and writes JSON the wrapper renders. design-system-health.json is a rendered output (Health Docs + DesignSystemHealth/TokenMatrix), so refresh-usage.sh re-runs this check with the other rendered extractors — see CONTEXT.md §STORAGE MAP.

Run it

SKILL=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
CORE=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
"$SKILL/scripts/validate-design-system.sh"       # → .storybook/design-system-health.json
# --emit-prompt  also outputs an LLM sub-agent prompt for semantic checks
#                (naming drift, semantic-vs-presentational, scale clarity)
"$CORE/scripts/scaffold-wrapper.sh" DesignSystemHealth   # → .storybook/wrappers/ (+ icons.tsx)

# Opt-in property→token-family check (off until a designer authors the rules file):
"$SKILL/scripts/check-property-tokens.py" --init   # scaffold design-system/lint/colors.json (+ schema)

What it checks

  • raw-color — hex/rgba/hsl literals in components (should be tokens). When the literal's value EQUALS an existing token's resolved value (e.g. #005596 on a tile whose siblings use bg-secondary → --secondary resolves to #005596), the fix is to adopt that exact token, not invent a new one — the finding names the matching token when it can resolve one, so the fix is "replace with --x", not "add a token".
  • undefined-token — components reference --foo that's not declared
  • scale-gap — spacing/type scale has unexpected jumps
  • unused-token — declared but never referenced (var, Tailwind utility, or custom @utility). Read from sb-inventory's single source (.storybook/project-inventory.json → tokens.map), not re-scanned here, so health, inventory, and the token views never disagree. Run sb-inventory first; if the file is absent this check is skipped.
  • design-md — a DESIGN.md (Google Labs YAML-tokens-plus-markdown, an increasingly common way to brief agents on a visual identity) is a claim, not ground truth — like AGENTS.md it drifts as the code changes, or was wrong from the start. The script finds it and cross-checks the colors it claims against the colors the code's CSS tokens actually declare, emitting design-md-drift for each claimed color the code doesn't define. Treat a present DESIGN.md as untrusted until reconciled.
  • property-token-family (opt-in) — a valid, declared token used on the wrong property family (e.g. color: var(--color-container) — a container token used as text color). raw-color catches "no token at all"; undefined-token catches "token doesn't exist"; this catches "real token, wrong slot." Off until a designer authors design-system/lint/colors.json (see below), so the zero-config default is preserved. Findings are warning/info only — an opt-in never flips a green CI build red.
  • runs stylelint if configured

Report summary (errors/warnings/info counts) and the top findings. The DesignSystemHealth wrapper renders severity with the shared icon set (no emoji).

The JSON it writes (shape — read these fields, don't re-derive). Each finding is {kind, severity, message, file?, line?, fix?}; summary rolls up the counts:

{
  "findings": [
    { "kind": "raw-color",       "severity": "error",   "file": "Button.tsx", "line": 42, "message": "#3b82f6 literal", "fix": "use --color-primary" },
    { "kind": "undefined-token", "severity": "error",   "file": "Card.tsx",   "line": 11, "message": "--surface-2 not declared" },
    { "kind": "design-md-drift", "severity": "warning", "message": "DESIGN.md claims #1e90ff; no token declares it" },
    { "kind": "unused-token",    "severity": "info",    "message": "--legacy-accent declared, never referenced" },
    { "kind": "scale-gap",       "severity": "info",    "message": "spacing jumps 16px → 48px" },
    { "kind": "property-token-family", "severity": "warning", "file": "Card.css", "line": 3, "message": "background uses --color-content — not allowed for 'background*' (expects --color-container / --color-canvas)", "fix": "Use one of --color-container, --color-canvas, or remap with /* color-lint-alias: … */" }
  ],
  "summary": { "total": 6, "errors": 2, "warnings": 2, "info": 2, "checksRun": ["raw-color", "undefined-token", "..."] }
}

Property→token-family rules (opt-in, designer-owned)

The which-token-family-belongs-on-which-property logic is design intent, so it lives in a designer-editable rules file — not in the script. The designer owns the rules; the dev owns the linter. Scaffold it once (writes the file and a JSON Schema so it's self-documenting in any editor):

"$SKILL/scripts/check-property-tokens.py" --init    # → design-system/lint/colors.json (+ colors.schema.json)
// design-system/lint/colors.json — owned by the designer
{
  "$schema": "./colors.schema.json",
  "propertyTokens": {
    "color":       ["--color-content"],                                   // exact property
    "background*": ["--color-container", "--color-canvas", "--color-backdrop"], // family (trailing *)
    "box-shadow":  ["--color-border", "--color-shadow"],
    "border*":     ["--color-border"]                                     // values are token PREFIXES
  }
}

Only properties listed are governed (opt in one property at a time). Escape hatches, in order of preference — both are read from your source files:

/* color-lint-alias: --color-theme --color-container */  /* file-level prefix remap: a token is
                                                            semantically right but lives in the "wrong"
                                                            family — treat this prefix as that one */
color: var(--color-theme-fg); /* color-lint-ignore */    /* per-line suppression — counted, and the
                                                            report nudges a refactor past 10. The number
                                                            growing is the signal: mute fast, fix slowly. */

The rules file is itself a claim, not ground truth (same stance as DESIGN.md): if a rule allows a token-prefix that no declared token matches, the script emits property-rules-drift so a stale config can't silently pass — or silently mis-flag.

Triage order — severity is the verdict, not the count: fix error first (raw-color → map to an existing token; undefined-token → declare it or fix the typo), reconcile warning (design-md-drift → hand to design-md; property-token-family → swap to an allowed token, or if the token is genuinely right add a color-lint-alias; property-rules-drift → fix the rules file), and report info as-is (unused-token / scale-gap / property-lint-suppressions are signals to verify, never auto-fixes — see Never below). A repo with 0 errors and 20 info findings is healthy.

Never (and the non-obvious why)

  • NEVER delete an unused-token on sight — it is info, not error. A static scan can't see a token consumed by a Tailwind utility (bg-[--brand]), a runtime var() built by string concat, or a sibling app in the monorepo. "Unused" means "I didn't find a reference," not "safe to remove."
  • NEVER treat scale-gap as a failure to fix — it's info. Real scales have intentional jumps (a display size far above body); "smoothing" them invents tokens nobody asked for and breaks the rhythm a designer chose.
  • NEVER trust DESIGN.md as ground truth — it's a claim, like AGENTS.md/CLAUDE.md, and drifts. Reconcile against design-md-drift first; an un-reconciled DESIGN.md will confidently lie about colors the code doesn't define.
  • NEVER "fix" a raw-color by inventing a new token — map it to an existing token. A fresh token per literal just relocates the mess into the token layer (more undefined-token noise next run).
  • NEVER hand-recompute this JSON by re-grepping — the browser can't shell out, so the script is the single source; cite its fields. Re-deriving by eye is how the counts drift from what the wrapper renders.

When to defer

For extraction (turning a messy codebase into tokens + components), this skill only measures. To actually extract, run /ds-runbook → /ds-audit → /ds-token-extract → /ds-component-extract, then come back here to verify health and to sb-stories for capture.

For a DESIGN.md that drifted (or to author/regenerate one from the code's real tokens), hand off to the design-md skill — it composes, extracts, and deep-audits the Google Labs DESIGN.md format. sb-health only flags the drift; design-md reconciles or rewrites the brief.

Next

If health is poor and the project is in transition, fix tokens (or route to ds-* skills) before authoring stories. Otherwise append a finding and continue to sb-stories.

Signals

GitHub stars
37
Last commit
Oct 2026

ahel review

  • K6low
    bundled executables the agent is told to run
  • K1binfo
    installs-packages (in scripts/scaffold-wrapper.sh)

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
sb-health
Source
github.com/strongeron/storybook-workbench