plugins/frontend-toolkit/skills/design-system-construction/SKILL.md
Build or audit a Tailwind + cva + shadcn/ui design system with two-tier tokens, variants, theming, and 4-state Storybook stories. Use when starting a new project, addressing UI inconsistency, onboarding a designer, adding dark mode / theming, when arbitrary values (w-[347px]) accumulate, or when component patterns repeat across 2+ places. Not for component-level extraction decisions (use component-quality) or WCAG conformance auditing (use accessibility-audit).
npx skillsauth add jaykim88/claude-ai-engineering design-system-constructionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Establish a reusable component library and design tokens so that UI consistency is enforced mechanically (via tokens and variants) rather than relied on by convention.
Universal — design tokens (colors, typography, spacing, radius), variant systems, 4-state stories (default/loading/error/empty), and "no arbitrary values" discipline apply to any UI framework. The default Procedure illustrates them with the Tailwind + cva + shadcn/ui idiom; the Other stacks section maps each to shadcn-vue / shadcn-svelte / spartan-ng and CSS-in-JS alternatives.
blue-500, gray-900, spacing units). Internal; components never reference these directly.primary, background, border, destructive, muted. Components consume only semantic tokens — this is what makes theming work: you remap the semantic→primitive layer once instead of editing every component.primary / primary-foreground) and verify each pair meets WCAG contrast at definition time — contrast is a token-design decision, not a per-component audit deferred to later.sm / md / lg / full) as named tokens.:root — see Implementation)1b. Make theming a token-map swap, not a component concern
.dark class or prefers-color-scheme<head>) to avoid a flash of the wrong theme (FOUC)Apply a typed variant system when a component has 3+ variants
size / intent / state axescn() — see Implementation)Adopt a component-primitive library you own the code of for common primitives (Button, Dialog, Form, Select, Toast)
components/ui/) so you can modify it freelyasChild slot (render-as-another-element — e.g. a Button rendering an <a>), className passthrough merged last-wins, forwarded refs/propsnpx shadcn@latest add …; Radix Slot / asChild — see Implementation)Write 4-state stories in a component workshop; run an a11y check on each
Default — happy pathLoading — pending / skeletonError — invalid input or failed fetchEmpty — no dataEliminate arbitrary values
grep -rE '\b(w|h|min-w|min-h|max-w|max-h|p[xytrbl]?|m[xytrbl]?|gap|top|left|right|bottom|text|bg|border|fill|stroke|grid-cols|grid-rows)-\[' src/
Document the system
COMPONENTS.md listing shared components and their usageVerify (validation loop)
| Tier | Examples | Action SLA |
|---|---|---|
| Critical | A semantic color pair fails WCAG contrast (ships an inaccessible default); components reference primitive/raw values (blue-500) so dark mode is broken | Block release; fix immediately |
| Major | Arbitrary values (w-[347px]) across many files; 3+ variants hand-rolled without a typed variant system; async component with no loading/error/empty story | Fix this sprint |
| Minor | Missing story for a static component; undocumented default variants; token naming drift | Schedule within 2 sprints |
Arbitrary value vs token + variant
// ❌ Arbitrary values — bypasses design system, untyped, hard to enforce consistency
<button className="w-[347px] h-[44px] bg-[#1a73e8] text-[14px] px-[16px]">
Save
</button>
// ✅ Tokens + cva variant — typed, consistent, override-friendly
const buttonVariants = cva('inline-flex items-center justify-center', {
variants: {
size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-6 text-lg' },
intent: { primary: 'bg-primary text-primary-foreground', ghost: 'bg-transparent' },
},
defaultVariants: { size: 'md', intent: 'primary' },
});
<button className={cn(buttonVariants({ size: 'md', intent: 'primary' }), className)}>
Save
</button>
blue-500) in component code* / *-foreground) meets WCAG AA contrast in both light and dark mapsw-[347px]) across 10+ files — design intent may require slightly different tokensdensity to all buttons) — coordinate with designertailwind.config.ts theme.extend + CSS variables in :root (one block per theme: light, dark)components/ui/ with shadcn-style primitives, one file per component<Component>.stories.tsx per component, 4 states (default / loading / error / empty) where applicabledocs/COMPONENTS.md with table — component name / variants / file path / Storybook linktheme.extend; semantic aliases as CSS variables in :root (e.g. --primary: var(--blue-600)) so components reference bg-primary, never bg-blue-600.dark class strategy with paired :root / .dark CSS-variable blocks; resolve theme in an inline <head> script to avoid FOUC (next-themes handles this on App Router)class-variance-authority (cva)cn() helper (clsx + tailwind-merge)npx shadcn@latest add <component> (you own the code); e.g. npx shadcn@latest add button dialog form select toast copies into components/ui/ (no npm dependency)Slot via the asChild prop — shadcn primitives already expose it@storybook/addon-a11ycva (works in Vue too) or vue-specific tailwind-variants; component library: shadcn-vue (port of shadcn/ui); Storybook with Vue 3 integrationtailwind-variants (cva-like API for any framework); component library: shadcn-svelte; Storybook 7+ supports Sveltew-[347px], text-[#1a2b3c]) — every framework that uses Tailwind suffers this; the grep pattern is framework-agnosticcomponent-quality — extraction criteria for promoting components into the design systemaccessibility-audit — design system must meet WCAG 2.2 AA out of the boxprimary, background, …) that aliases it — components reference only semantic tokens, so theming/dark-mode is a one-map swap instead of a per-component edit. Bake WCAG contrast into the */*-foreground pairs at token-definition time. Variants belong in CVA (typed and composable); arbitrary values are a smell — promote to tokens or compound variants. The "ban arbitrary values" rule is a community practice (Vercel/Infinum handbooks), not in official docs — encode it as a project rule.development
Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).
development
Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).
development
Inventory and prioritize technical debt — TODO/FIXME/HACK, any usage, deprecated APIs, untested logic — with impact × effort matrix. Use at quarter start, before a refactoring sprint, when a new teammate joins, or when feature velocity slows. Not for actually paying down debt (use code-refactoring) or recording a migration approach (use decision-records) — this only inventories and prioritizes.
development
Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).