set-up-design-system
Skill7 starsChanged 16 days agoMITView on GitHub
Use when establishing a design system — Tailwind v4 @theme tokens, class-based dark mode from a persisted theme store (CSP-safe pre-paint script), a cn() merger, and cva variant primitives; shadcn/ui is the optional component registry.
- Installs packages
1 other project uses a version of this.
Adopted by
Most recently taken up by
- VelimirMueller/claude-skillsword for word, first seen Oct 11, 2026
Other versions1 project uses a version of this.
Word for word · 1
- VelimirMueller/claude-skills★ 7100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository156 other files it gives its agents.
- skills/swift/swiftui-design-principles/AGENTS.mdAGENTS.md
- skills/swift/ios-code-audit/CLAUDE.mdCLAUDE.md
- skills/swift/swift-focusengine-pro/llms.txtllms.txt
- build-llm-seamSkill
- build-mcp-serverSkill
- build-rag-pipelineSkill
- secure-llm-featuresSkill
- set-up-llm-evalsSkill
- build-nextjs-backendSkill
- build-supabase-edge-functionSkill
- configure-backend-testsSkill
- design-http-apiSkill
- harden-backendSkill
- scaffold-fastapi-serviceSkill
- scaffold-go-serviceSkill
- scaffold-hono-serviceSkill
- secure-supabase-rlsSkill
- set-up-backend-authSkill
- set-up-background-jobsSkill
- set-up-databaseSkill
- set-up-nextjs-supabase-authSkill
- set-up-observabilitySkill
- set-up-supabaseSkill
- build-cliSkill
- release-cliSkill
- set-up-dev-toolchainSkill
- audit-securitySkill
- audit-toolchainSkill
- extend-skillsetSkill
- promote-releaseSkill
- set-up-stack-profileSkill
- write-commit-messagesSkill
- write-pull-requestsSkill
- analyze-complexitySkill
- apply-algorithmsSkill
- apply-design-patternsSkill
- apply-design-principlesSkill
- apply-solidSkill
- choose-data-structureSkill
- debug-systematicallySkill
- enforce-dependency-ruleSkill
- refactor-safelySkill
- slice-and-estimateSkill
- write-clean-testsSkill
- write-readable-codeSkill
- clean-frontend-scaffoldingSkill
- configure-accessibilitySkill
- configure-analyticsSkill
- configure-ciSkill
- configure-error-trackingSkill
- configure-lintingSkill
- configure-test-stackSkill
- configure-typescriptSkill
- create-moduleSkill
- optimize-performanceSkill
- scaffold-frontend-projectSkill
- set-up-authSkill
- set-up-document-headSkill
- set-up-error-boundariesSkill
- set-up-feature-flagsSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 17
Read frontend.framework (React store = Zustand, Vue = Pinia) and package_manager (commands use pnpm).
- Distinctive: line 20
No tokens/primitives → full setup.
- Very specific: line 59
More than one theme (brand × light/dark)?: Keep utilities fixed and swap token *values*.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Set Up Design System
1. Audit current state
cat .claude/stack-profile.md 2>/dev/null || cat ~/.claude/stack-profile.md 2>/dev/null # frontend.framework, package_manager
grep -E '"(class-variance-authority|clsx|tailwind-merge|tailwindcss|tailwind-variants)"' package.json 2>/dev/null
grep -n "@theme\|@custom-variant" src/index.css src/style.css 2>/dev/null
ls components.json 2>/dev/null # shadcn/ui markerRead frontend.framework (React store = Zustand, Vue = Pinia) and package_manager (commands use pnpm). Detect existing tokens, a variant lib, and dark-mode wiring. Prerequisites: Tailwind v4 installed (scaffold-frontend-project), @/ alias, and set-up-state-management (the theme toggle is a UI-state store).
2. Decide what to do
- No tokens/primitives → full setup.
- Tokens present but ad-hoc class strings → add
cn()+ cva variants. - shadcn/ui already initialized → adopt its
cn/cva; just add the token layer + theme store.
3. Detect framework
React → primitives as .tsx with cva. Vue → primitives as SFCs using the same cva class strings. cn() and @theme are framework-agnostic.
4. Install
pnpm add class-variance-authority clsx tailwind-mergetailwind-merge must be v3 or newer for Tailwind 4 class names. Why cva + cn(): design-tokens.md.
(Optional, React: pnpm dlx shadcn@latest init — generates owned, cva-based primitives into your tree. Then skip hand-writing step 7.)
5. Define tokens in @theme (CSS-first)
Tailwind v4 reads tokens from CSS and generates the matching utilities (bg-brand-600, rounded-card, …). Use semantic names and oklch for perceptually-even colors.
/* src/index.css (or src/style.css) */
@import "tailwindcss";
/* class-based dark mode: `dark:` applies under .dark on <html> */
@custom-variant dark (&:where(.dark, .dark *));
/* native form controls and scrollbars follow the theme */
:root { color-scheme: light; }
.dark { color-scheme: dark; }
@theme {
--color-brand-50: oklch(0.97 0.02 255);
--color-brand-500: oklch(0.62 0.19 255);
--color-brand-600: oklch(0.54 0.20 255);
--color-brand-700: oklch(0.47 0.19 255);
--radius-card: 0.75rem;
--font-sans: "Inter", system-ui, sans-serif;
}More than one theme (brand × light/dark)? Keep utilities fixed and swap token values. Semantic tokens point at CSS variables via @theme inline; each theme redefines the variables:
/* src/styles/tokens.css */
@theme inline {
--color-bg: var(--bg);
--color-fg: var(--fg);
--color-accent: var(--accent);
--color-ring: var(--ring);
}
:root, [data-theme="default"] { --bg: #fafafa; --fg: #09090b; --accent: #18181b; --ring: #18181b; }
.dark, [data-theme="default"].dark { --bg: #09090b; --fg: #f4f4f5; --accent: #fafafa; --ring: #e4e4e7; }
[data-theme="neon"].dark { --bg: #05040d; --fg: #e6f1ff; --accent: #00fff7; --ring: #00fff7; }Every theme defines both modes ([data-theme="neon"] for light is omitted here for brevity); a missing block silently falls back to :root. Components use bg-bg text-fg ring-ring only — a new theme is one CSS block, no component change. Check every fg/bg pair for 4.5:1 per theme and mode (configure-accessibility).
6. The cn() class merger
// src/utils/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/** Merge class lists, with later Tailwind utilities winning over earlier ones. */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}7. A variant-driven primitive (atom)
React
// src/components/atoms/Button/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import type { ButtonHTMLAttributes } from 'react';
import { cn } from '@/utils/cn';
const button = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-600 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
solid: 'bg-brand-600 text-white hover:bg-brand-700',
outline: 'border border-brand-600 text-brand-600 hover:bg-brand-50 dark:border-brand-500 dark:text-brand-500 dark:hover:bg-brand-500/10',
ghost: 'text-brand-600 hover:bg-brand-50 dark:text-brand-500 dark:hover:bg-brand-500/10',
},
size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-6 text-lg' },
},
defaultVariants: { variant: 'solid', size: 'md' },
},
);
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof button>;
export function Button({ className, variant, size, ...props }: ButtonProps) {
return <button type="button" className={cn(button({ variant, size }), className)} {...props} />; // type before the spread: a form's submit button says type="submit" itself
}Contrast is part of the token choice: white on brand-600 is 5.1:1, but white on brand-500 is only 3.7:1 (fails AA 4.5:1), so hover darkens to brand-700; brand-600 text on a near-black surface is about 4:1, so dark mode uses brand-500. Tailwind 4 renamed the old outline-none to outline-hidden (it keeps a transparent outline visible in forced-colors mode); the new outline-none removes it entirely. Recheck contrast whenever a token changes (configure-accessibility).
For Vue, mirror with an SFC: define the same button cva map, then :class="cn(button({ variant, size }), $attrs.class)".
8. Dark mode = persisted UI state, applied before first paint (no FOUC)
The theme is UI state — a small persisted store — but it must reach <html> before the bundle paints, or dark-mode users get a flash of light. Two parts:
1. Pre-paint, as a same-origin file — public/theme-init.js, loaded from <head> before the bundle:
<!-- index.html <head>: classic blocking script on purpose; a module or defer script runs after first paint -->
<script src="/theme-init.js"></script>// public/theme-init.js
(() => {
let t = 'system';
try { t = JSON.parse(localStorage.getItem('theme') ?? '{}')?.state?.theme ?? 'system'; } catch {}
const dark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
})();It is a file, not an inline <script>, because set-up-security-headers ships script-src 'self' without 'unsafe-inline'; an inline theme script would be blocked in production and dark-mode users would see a flash of light. (It parses Zustand's persisted shape stored under the theme key — keep that key in sync with the store.)
2. The store — 'light' | 'dark' | 'system', defaulting to system (a named theme, if any, is a second field applied as data-theme on <html> by the same theme-init.js):
// src/stores/useThemeStore.ts (React — Zustand)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type Theme = 'light' | 'dark' | 'system';
type ThemeState = { theme: Theme; setTheme: (t: Theme) => void };
export const useThemeStore = create<ThemeState>()(
persist((set) => ({ theme: 'system', setTheme: (theme) => set({ theme }) }), { name: 'theme' }),
);
export const resolveTheme = (t: Theme): 'light' | 'dark' =>
t === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : t;// apply on change, and react to OS changes while on 'system'
useEffect(() => {
const apply = () =>
document.documentElement.classList.toggle('dark', resolveTheme(useThemeStore.getState().theme) === 'dark');
apply();
const unsub = useThemeStore.subscribe(apply);
const mq = matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', apply);
return () => { unsub(); mq.removeEventListener('change', apply); };
}, []);Vue: the same store as a Pinia setup-store + a watchEffect + the same matchMedia listener; theme-init.js reads the theme key, so a Pinia persistence plugin must write the same key and shape ({"state":{"theme":"dark"}}) or theme-init.js must be adapted to it.
9. Verify
pnpm typecheck
pnpm dev<Button variant="outline" size="lg"> renders with tokens; toggling the theme store flips .dark on <html> and dark: utilities apply. Reload preserves the choice (persisted).
References
- ./design-tokens.md — token naming, cva + cn rationale, dark-mode strategy, theme-is-UI-state, shadcn/ui, when to deviate.
- ../_shared/glossary.md — theme as UI state.
- ../_shared/conventions.md —
@/alias, atoms layer,stores/.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

