Design System Skill
Patterns for analyzing design tokens and mapping between mocks and NextSpark themes.
Fundamental Principle
THE DESIGN SYSTEM IS THEME-DEPENDENT.
All values in this skill are EXAMPLES from the default theme.
You MUST read the actual theme's globals.css to get real values.
bash
1# Determine active theme
2grep "NEXT_PUBLIC_ACTIVE_THEME" .env .env.local
3
4# Read theme tokens
5cat contents/themes/{THEME}/styles/globals.css
Theme Token Locations
contents/themes/{THEME}/
├── styles/
│ ├── globals.css # CSS variables (:root and .dark)
│ └── components.css # Component-specific styles
├── config/
│ └── theme.config.ts # Theme metadata
CSS Variable Structure
Light Mode (:root)
css
1:root {
2 /* Surface Colors */
3 --background: oklch(1 0 0); /* Page background */
4 --foreground: oklch(0.145 0 0); /* Primary text */
5 --card: oklch(1 0 0); /* Card surfaces */
6 --card-foreground: oklch(0.145 0 0); /* Card text */
7 --popover: oklch(1 0 0); /* Dropdowns */
8 --popover-foreground: oklch(0.145 0 0);
9
10 /* Interactive Colors */
11 --primary: oklch(0.205 0 0); /* Primary actions */
12 --primary-foreground: oklch(0.985 0 0);
13 --secondary: oklch(0.97 0 0); /* Secondary actions */
14 --secondary-foreground: oklch(0.205 0 0);
15 --accent: oklch(0.97 0 0); /* Highlights */
16 --accent-foreground: oklch(0.205 0 0);
17
18 /* State Colors */
19 --muted: oklch(0.97 0 0); /* Muted backgrounds */
20 --muted-foreground: oklch(0.556 0 0); /* Placeholder text */
21 --destructive: oklch(0.577 0.245 27); /* Error/danger */
22 --destructive-foreground: oklch(1 0 0);
23
24 /* Border & Input */
25 --border: oklch(0.922 0 0);
26 --input: oklch(0.922 0 0);
27 --ring: oklch(0.708 0 0); /* Focus rings */
28
29 /* Radius */
30 --radius: 0.5rem;
31}
Dark Mode (.dark)
css
1.dark {
2 --background: oklch(0.145 0 0); /* Inverted */
3 --foreground: oklch(0.985 0 0);
4 --card: oklch(0.145 0 0);
5 --card-foreground: oklch(0.985 0 0);
6
7 --primary: oklch(0.922 0 0); /* Adjusted for dark */
8 --primary-foreground: oklch(0.205 0 0);
9
10 --muted: oklch(0.269 0 0);
11 --muted-foreground: oklch(0.708 0 0);
12
13 --border: oklch(0.269 0 0);
14 --input: oklch(0.269 0 0);
15}
Mocks often use HEX/RGB, themes use OKLCH.
HEX to OKLCH Mapping
| Mock (HEX) | Approximate OKLCH | Notes |
|---|
#ffffff | oklch(1 0 0) | Pure white |
#000000 | oklch(0 0 0) | Pure black |
#137fec | oklch(0.55 0.2 250) | Blue primary |
#101922 | oklch(0.15 0.02 260) | Dark background |
#00d4ff | oklch(0.75 0.15 200) | Cyan accent |
Similarity Calculation
Compare colors by:
- Lightness (L) - Most important, weight 0.5
- Chroma (C) - Saturation, weight 0.3
- Hue (H) - Color angle, weight 0.2
similarity = 1 - (
0.5 * |L1 - L2| +
0.3 * |C1 - C2| / maxChroma +
0.2 * |H1 - H2| / 360
)
Token Categories
Background Tokens
| Token | Tailwind Class | Usage |
|---|
--background | bg-background | Page background |
--card | bg-card | Card surfaces |
--popover | bg-popover | Dropdowns, menus |
--muted | bg-muted | Subtle backgrounds |
--accent | bg-accent | Hover states |
--primary | bg-primary | Primary buttons |
--secondary | bg-secondary | Secondary buttons |
--destructive | bg-destructive | Error states |
Foreground Tokens
| Token | Tailwind Class | Usage |
|---|
--foreground | text-foreground | Primary text |
--card-foreground | text-card-foreground | Card text |
--muted-foreground | text-muted-foreground | Secondary text |
--primary-foreground | text-primary-foreground | On primary bg |
--destructive-foreground | text-destructive-foreground | On error bg |
Border Tokens
| Token | Tailwind Class | Usage |
|---|
--border | border-border | Default borders |
--input | border-input | Input borders |
--ring | ring-ring | Focus rings |
Mapping Process
From Tailwind config or inline styles:
javascript
1// From mock's tailwind.config
2const mockTokens = {
3 colors: {
4 primary: '#137fec',
5 'bg-dark': '#101922',
6 accent: '#00d4ff'
7 }
8}
Step 2: Read Theme Tokens
bash
1# Extract all CSS variables
2grep -E "^\s*--" contents/themes/{theme}/styles/globals.css
Step 3: Create Mapping
For each mock token:
- Check exact match (hex → hex)
- Check semantic match (primary → --primary)
- Calculate color similarity
- Flag gaps if no good match
Step 4: Document Gaps
json
1{
2 "gaps": [
3 {
4 "mockValue": "#ff5722",
5 "mockUsage": "accent icons",
6 "closestToken": "--destructive",
7 "similarity": 0.72,
8 "recommendation": "USE_CLOSEST or ADD_TOKEN"
9 }
10 ]
11}
json
1{
2 "theme": "default",
3 "themeGlobalsPath": "contents/themes/default/styles/globals.css",
4 "analyzedAt": "2025-01-09T12:00:00Z",
5
6 "themeTokens": {
7 "colors": {
8 "--background": "oklch(1 0 0)",
9 "--foreground": "oklch(0.145 0 0)",
10 "--primary": "oklch(0.205 0 0)",
11 "--secondary": "oklch(0.97 0 0)",
12 "--accent": "oklch(0.97 0 0)",
13 "--muted": "oklch(0.97 0 0)",
14 "--destructive": "oklch(0.577 0.245 27.325)"
15 },
16 "radius": "0.5rem",
17 "fonts": {
18 "sans": "var(--font-sans)",
19 "mono": "var(--font-mono)"
20 }
21 },
22
23 "mockTokens": {
24 "colors": {
25 "primary": "#137fec",
26 "background-dark": "#101922",
27 "accent": "#00d4ff",
28 "text-light": "#ffffff",
29 "text-muted": "#94a3b8"
30 }
31 },
32
33 "colorMapping": [
34 {
35 "id": "color-1",
36 "mockValue": "#137fec",
37 "mockName": "primary",
38 "mockUsage": ["buttons", "links", "focus rings"],
39 "themeToken": "--primary",
40 "themeValue": "oklch(0.205 0 0)",
41 "tailwindClass": "bg-primary text-primary-foreground",
42 "matchType": "semantic",
43 "similarity": 0.65,
44 "notes": "Theme primary is darker, mock is more vibrant blue"
45 },
46 {
47 "id": "color-2",
48 "mockValue": "#101922",
49 "mockName": "background-dark",
50 "mockUsage": ["hero background", "footer"],
51 "themeToken": "--background",
52 "themeValue": "oklch(0.145 0 0)",
53 "tailwindClass": "bg-background",
54 "matchType": "closest",
55 "similarity": 0.88,
56 "notes": "Use dark mode or bg-gray-900"
57 }
58 ],
59
60 "typographyMapping": [
61 {
62 "mockFont": "Inter",
63 "themeToken": "--font-sans",
64 "tailwindClass": "font-sans",
65 "matchType": "exact"
66 }
67 ],
68
69 "spacingMapping": [
70 {
71 "mockValue": "24px",
72 "tailwindClass": "p-6",
73 "matchType": "exact"
74 }
75 ],
76
77 "radiusMapping": [
78 {
79 "mockValue": "8px",
80 "themeToken": "--radius",
81 "themeValue": "0.5rem",
82 "tailwindClass": "rounded-lg",
83 "matchType": "exact"
84 }
85 ],
86
87 "gaps": [
88 {
89 "type": "color",
90 "mockValue": "#00d4ff",
91 "mockName": "accent",
92 "mockUsage": ["terminal prompt", "code highlights"],
93 "closestToken": "--primary",
94 "similarity": 0.45,
95 "recommendations": [
96 {
97 "option": "A",
98 "action": "Use --primary",
99 "impact": "Loses cyan accent, uses theme primary"
100 },
101 {
102 "option": "B",
103 "action": "Add --accent-cyan to theme",
104 "impact": "Requires theme modification"
105 },
106 {
107 "option": "C",
108 "action": "Use inline text-[#00d4ff]",
109 "impact": "Not recommended, breaks theming"
110 }
111 ]
112 }
113 ],
114
115 "summary": {
116 "totalMockTokens": 12,
117 "mapped": 10,
118 "gaps": 2,
119 "overallCompatibility": 0.83,
120 "recommendation": "PROCEED_WITH_GAPS"
121 }
122}
For the BLOCKS workflow, an additional output is generated to guide block development:
json
1{
2 "mockPath": "mocks/",
3 "analyzedAt": "2026-01-12T12:00:00Z",
4 "workflow": "BLOCKS",
5
6 "existingBlocks": [
7 {
8 "name": "hero-simple",
9 "similarity": 0.85,
10 "matchReason": "Similar layout and components"
11 },
12 {
13 "name": "hero-centered",
14 "similarity": 0.72,
15 "matchReason": "Centered text, different background"
16 }
17 ],
18
19 "decision": {
20 "type": "new" | "variant" | "existing",
21 "blockName": "hero-terminal",
22 "baseBlock": "hero-simple",
23 "reasoning": "Requires custom terminal animation component not in existing blocks"
24 },
25
26 "blockSpec": {
27 "name": "hero-terminal",
28 "category": "hero",
29 "fields": [
30 {"name": "title", "type": "text", "required": true},
31 {"name": "subtitle", "type": "text", "required": false},
32 {"name": "primaryCta", "type": "link", "required": true},
33 {"name": "secondaryCta", "type": "link", "required": false},
34 {"name": "terminalContent", "type": "textarea", "required": true}
35 ],
36 "customComponents": ["TerminalAnimation"],
37 "estimatedComplexity": "medium"
38 },
39
40 "developmentNotes": [
41 "Terminal animation requires custom React component",
42 "Use existing Button component for CTAs",
43 "Background gradient matches theme --background token"
44 ]
45}
Decision Types
| Type | When to Use | Action |
|---|
existing | Mock matches existing block 90%+ | Use existing block, no changes |
variant | Mock matches but needs minor additions | Extend existing block with new variant |
new | Mock requires significant new functionality | Create new block from scratch |
Workflow Integration
| Workflow | ds-mapping.json | block-plan.json | When Generated |
|---|
| BLOCKS | Yes | Yes | Phase 1 (Mock Analysis) |
| TASK | Yes (if mock) | No | Phase 0.6 (if mock selected) |
| STORY | Yes (if mock) | No | Phase 0.6 (if mock selected) |
| TWEAK | No | No | N/A |
Reusability
This skill applies to ANY design-to-code conversion:
- Landing pages (mocks → blocks)
- Email templates (design → HTML)
- PDF templates (design → React-PDF)
- Marketing materials
Generating globals.css from Mock (One-Time Setup)
Use this section when initializing a theme from a design mock. This is typically a one-time setup task during theme creation.
When to Use
- New theme creation from design mock
- Theme initialization via
how-to:customize-theme command
- Converting a purchased template to NextSpark theme
Step 1: Convert HEX to OKLCH
For each color in mock's Tailwind config, convert from HEX to OKLCH:
javascript
1function hexToOklch(hex) {
2 // Remove # if present
3 hex = hex.replace('#', '')
4
5 // Parse RGB
6 const r = parseInt(hex.substr(0, 2), 16) / 255
7 const g = parseInt(hex.substr(2, 2), 16) / 255
8 const b = parseInt(hex.substr(4, 2), 16) / 255
9
10 // Convert to linear RGB
11 const rL = r <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
12 const gL = g <= 0.04045 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
13 const bL = b <= 0.04045 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
14
15 // Approximate OKLCH
16 const L = 0.4122 * rL + 0.5363 * gL + 0.0514 * bL
17 const lightness = Math.cbrt(L)
18
19 // Chroma and hue calculation
20 const max = Math.max(r, g, b)
21 const min = Math.min(r, g, b)
22 const chroma = (max - min) * 0.3
23
24 let hue = 0
25 if (max !== min) {
26 if (max === r) hue = 60 * (((g - b) / (max - min)) % 6)
27 else if (max === g) hue = 60 * ((b - r) / (max - min) + 2)
28 else hue = 60 * ((r - g) / (max - min) + 4)
29 if (hue < 0) hue += 360
30 }
31
32 return `oklch(${lightness.toFixed(4)} ${chroma.toFixed(4)} ${hue.toFixed(0)})`
33}
Step 2: Generate Dark Mode (Invert Lightness)
For dark mode, invert the lightness (L) value:
javascript
1function invertLightnessOklch(oklchValue) {
2 // Parse oklch(L C H)
3 const match = oklchValue.match(/oklch\(([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\)/)
4 if (!match) return oklchValue
5
6 const L = parseFloat(match[1])
7 const C = parseFloat(match[2])
8 const H = parseFloat(match[3])
9
10 // Invert lightness: L' = 1 - L
11 const invertedL = 1 - L
12
13 return `oklch(${invertedL.toFixed(4)} ${C.toFixed(4)} ${H.toFixed(0)})`
14}
Which tokens to invert:
--background ↔ --foreground (swap)
--card ↔ --card-foreground (swap)
--popover ↔ --popover-foreground (swap)
--muted → invert
--muted-foreground → invert
--border, --input → invert
--primary, --secondary, --accent → typically keep similar or slightly adjust
Step 3: globals.css Template
css
1/**
2 * Theme: {theme}
3 * Generated from mock: {mockPath}
4 * Date: {timestamp}
5 *
6 * NOTE: Dark mode was auto-generated by inverting lightness values.
7 * Review and adjust .dark {} section as needed for your brand.
8 */
9
10:root {
11 /* Surface Colors */
12 --background: {oklch from mock};
13 --foreground: {oklch from mock};
14 --card: {oklch from mock or default};
15 --card-foreground: {oklch from mock or default};
16 --popover: {oklch from mock or default};
17 --popover-foreground: {oklch from mock or default};
18
19 /* Interactive Colors */
20 --primary: {oklch from mock};
21 --primary-foreground: {calculated contrast};
22 --secondary: {oklch from mock or default};
23 --secondary-foreground: {calculated contrast};
24 --accent: {oklch from mock or default};
25 --accent-foreground: {calculated contrast};
26
27 /* State Colors */
28 --muted: {oklch from mock or default};
29 --muted-foreground: {oklch from mock or default};
30 --destructive: oklch(0.577 0.245 27.325);
31 --destructive-foreground: oklch(1 0 0);
32
33 /* Border & Input */
34 --border: {oklch from mock or default};
35 --input: {oklch from mock or default};
36 --ring: {oklch from mock or default};
37
38 /* Chart Colors */
39 --chart-1: oklch(0.81 0.1 252);
40 --chart-2: oklch(0.62 0.19 260);
41 --chart-3: oklch(0.55 0.22 263);
42 --chart-4: oklch(0.49 0.22 264);
43 --chart-5: oklch(0.42 0.18 266);
44
45 /* Sidebar */
46 --sidebar: {based on background};
47 --sidebar-foreground: {based on foreground};
48 --sidebar-primary: {based on primary};
49 --sidebar-primary-foreground: {based on primary-foreground};
50 --sidebar-accent: {based on accent};
51 --sidebar-accent-foreground: {based on accent-foreground};
52 --sidebar-border: {based on border};
53 --sidebar-ring: {based on ring};
54
55 /* Typography */
56 --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
57 --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
58 --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
59
60 /* Design Tokens */
61 --radius: 0.625rem;
62 --spacing: 0.25rem;
63
64 /* Shadows */
65 --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
66 --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
67 --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
68 --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
69 --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
70 --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
71 --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
72 --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
73}
74
75/* =============================================
76 DARK MODE (Auto-generated by inverting lightness)
77 Review and adjust as needed for your brand
78 ============================================= */
79
80.dark {
81 --background: {inverted};
82 --foreground: {inverted};
83 --card: {inverted};
84 --card-foreground: {inverted};
85 --popover: {inverted};
86 --popover-foreground: {inverted};
87 --primary: {adjusted for dark};
88 --primary-foreground: {adjusted for dark};
89 --secondary: {inverted};
90 --secondary-foreground: {inverted};
91 --muted: {inverted};
92 --muted-foreground: {inverted};
93 --accent: {inverted};
94 --accent-foreground: {inverted};
95 --destructive: oklch(0.704 0.191 22.216);
96 --destructive-foreground: oklch(0.985 0 0);
97 --border: {inverted};
98 --input: {inverted};
99 --ring: {inverted};
100 --sidebar: {inverted};
101 --sidebar-foreground: {inverted};
102 --sidebar-primary: {adjusted};
103 --sidebar-primary-foreground: {adjusted};
104 --sidebar-accent: {inverted};
105 --sidebar-accent-foreground: {inverted};
106 --sidebar-border: {inverted};
107 --sidebar-ring: {inverted};
108}
109
110/* =============================================
111 TAILWIND v4 THEME MAPPING
112 ============================================= */
113
114@theme inline {
115 --color-background: var(--background);
116 --color-foreground: var(--foreground);
117 --color-card: var(--card);
118 --color-card-foreground: var(--card-foreground);
119 --color-popover: var(--popover);
120 --color-popover-foreground: var(--popover-foreground);
121 --color-primary: var(--primary);
122 --color-primary-foreground: var(--primary-foreground);
123 --color-secondary: var(--secondary);
124 --color-secondary-foreground: var(--secondary-foreground);
125 --color-muted: var(--muted);
126 --color-muted-foreground: var(--muted-foreground);
127 --color-accent: var(--accent);
128 --color-accent-foreground: var(--accent-foreground);
129 --color-destructive: var(--destructive);
130 --color-destructive-foreground: var(--destructive-foreground);
131 --color-border: var(--border);
132 --color-input: var(--input);
133 --color-ring: var(--ring);
134 --color-chart-1: var(--chart-1);
135 --color-chart-2: var(--chart-2);
136 --color-chart-3: var(--chart-3);
137 --color-chart-4: var(--chart-4);
138 --color-chart-5: var(--chart-5);
139 --color-sidebar: var(--sidebar);
140 --color-sidebar-foreground: var(--sidebar-foreground);
141 --color-sidebar-primary: var(--sidebar-primary);
142 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
143 --color-sidebar-accent: var(--sidebar-accent);
144 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
145 --color-sidebar-border: var(--sidebar-border);
146 --color-sidebar-ring: var(--sidebar-ring);
147
148 --font-sans: var(--font-sans);
149 --font-mono: var(--font-mono);
150 --font-serif: var(--font-serif);
151
152 --radius-sm: calc(var(--radius) - 4px);
153 --radius-md: calc(var(--radius) - 2px);
154 --radius-lg: var(--radius);
155 --radius-xl: calc(var(--radius) + 4px);
156
157 --shadow-2xs: var(--shadow-2xs);
158 --shadow-xs: var(--shadow-xs);
159 --shadow-sm: var(--shadow-sm);
160 --shadow: var(--shadow);
161 --shadow-md: var(--shadow-md);
162 --shadow-lg: var(--shadow-lg);
163 --shadow-xl: var(--shadow-xl);
164 --shadow-2xl: var(--shadow-2xl);
165}
Generation Checklist
tailwind-theming - Detailed Tailwind CSS patterns
shadcn-theming - shadcn/ui theme customization
mock-analysis - For extracting mock tokens
page-builder-blocks - For applying tokens to blocks
block-decision-matrix - For block new/variant/existing decisions