A practical guide to building a production-grade design system using Claude Code as your design partner. Based on the actual process that produced Arctic Midnight.
This is not theory. This is what we did. Here is how.
Everything starts with data, not design. Your brand config is a set of JSON files that describe your visual identity in machine-readable format. Claude reads these at the start of every session.
Define your colors, fonts, spacing, grain intensity, and easing curve in a single JSON file. This becomes the source of truth that drives everything downstream.
{
"theme_name": "Arctic Midnight",
"colors": {
"background": { "light": "#F9F9F9", "dark": "#0A0A0A" },
"foreground": { "light": "#0A0A0A", "dark": "#F9F9F9" },
"primary": { "light": "#7C3AED", "dark": "#8B5CF6" },
"aurora": [
{ "name": "Malachite", "hex": "#059669" },
{ "name": "Deep Cyan", "hex": "#0891B2" },
{ "name": "Modh Purple", "hex": "#7C3AED" },
{ "name": "Dracula Red", "hex": "#BE123C" },
{ "name": "Amber", "hex": "#D97706" }
]
},
"grain": { "web": 0.03, "mesh": 0.25, "video": 0.15 },
"easing": "cubic-bezier(0.23, 1, 0.32, 1)"
}Read brand-config/visual.json, then update globals.css to express every color as a CSS custom property. Add utility tokens for sub-pixel borders, hover states, and backdrop effects. Support light and dark modes.
Express every token as a CSS custom property in globals.css. Wire them into Tailwind via @theme inline. Now bg-background, text-primary all resolve to your tokens.
:root {
--background: #F9F9F9;
--foreground: #0A0A0A;
--primary: #7C3AED;
--aurora-1: #059669;
--aurora-2: #0891B2;
--aurora-3: #7C3AED;
--aurora-4: #BE123C;
--aurora-5: #D97706;
--sub-border: rgba(10, 10, 10, 0.05);
--magnetic-wash: rgba(124, 58, 237, 0.08);
}
.dark {
--background: #0A0A0A;
--foreground: #F9F9F9;
--primary: #8B5CF6;
}No magic strings. No hardcoded hex values in components. Change the JSON, regenerate, and every surface updates.
This is where most design systems stop at 'colors and fonts.' Arctic Midnight goes further: it defines the texture, depth, and grain of every surface.
SVG feTurbulence noise overlay in your root layout. Fixed position, full viewport, pointer-events none. Film stock feel, barely perceptible at 0.03 opacity.
<svg className="grain-overlay" aria-hidden="true">
<filter id="grain">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
/>
</filter>
<rect width="100%" height="100%" filter="url(#grain)" />
</svg>Grain budget: 0.03 for web UI. 0.25 for mesh gradients. 0.15 for video. Flat surfaces are cheap. Textured surfaces are premium.
The signature visual. Layer 5-7 radial-gradient ellipses at varying positions and sizes. Each gradient uses an aurora color at low opacity. Add dedicated black radial gradients for depth. Cover with SVG grain at 0.25 opacity.
Critical learning: we started with linear-gradient and conic-gradient. They created hard, directional bands. Visible straight lines.
The fix was switching entirely to radial-gradient ellipses. Ellipses blend organically because their edges are always curved. No hard edges. No banding. Color emerges through darkness.
Create an aurora-mesh.tsx component. Pure CSS, no JS animation. Use 5-7 radial-gradient ellipses with aurora colors at 0.3-0.5 opacity on a near-black background. Add 2 dedicated black radial gradients for depth. SVG grain overlay at 0.25 opacity, baseFrequency 0.85, 5 octaves. Create 5 variants: aurora, ember, frost, moss, dracula.
Port the blob positions into a requestAnimationFrame loop. Each blob drifts via Math.sin/cos with unique phase offsets. Falls back to static on reduced motion.
const animate = useCallback(() => {
const t = performance.now() * speed
const gradients = blobs.map((blob, i) => {
const phase = i * 1.3
const cx = blob.cx + Math.sin(t + phase) * blob.dx
const cy = blob.cy + Math.cos(t * 0.7 + phase) * blob.dy
return buildBlob(blob, cx, cy)
})
el.style.backgroundImage = gradients.join(', ')
rafRef.current = requestAnimationFrame(animate)
}, [config, speed])Every interactive element needs physics. Not just color changes. Define the patterns once, use them everywhere.
The magnetic button does not just change color on hover. It scales 1.02x and a purple wash slides up from underneath via ::after pseudo-element.
.magnetic-btn {
position: relative;
overflow: hidden;
transition: transform 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.magnetic-btn:hover { transform: scale(1.02); }
.magnetic-btn::after {
content: '';
position: absolute;
inset: 0;
background: var(--magnetic-wash);
transform: translateY(100%);
transition: transform 0.45s cubic-bezier(0.23, 1, 0.32, 1);
}
.magnetic-btn:hover::after { transform: translateY(0); }Button content must be wrapped in <span className="relative z-10"> so the wash slides underneath. Every interactive element needs cursor-pointer.
Two fonts. Strict rules.
| Context | Font | Weight | Tracking |
|---|---|---|---|
| Headlines | Jakarta Sans | font-medium (500) | -0.04em |
| Body | Jakarta Sans | normal (400) | -0.02em |
| Labels, numbers | Geist Mono | font-medium on dark | 0.15em |
| Code, paths | Geist Mono | normal | default |
Never use font-bold or font-semibold.
Boldness comes from size and tracking, not weight.
This is where vibe coding diverges from traditional design. You do not spec everything upfront. You build, look, react, refine.
Tell Claude what to build
Be specific about the surface, vague about the details. "Build a section that shows the aurora palette swatches with a mesh gradient demo below."
Look at the result
Run pnpm dev, open the page, use your eyes. No amount of description replaces visual inspection.
React with direction
Instead of "change the opacity to 0.7", say "the mono text is nearly invisible on the mesh." Name problems, not solutions.
Dial the knobs
"Darker." "Heavier on the black." "Dial up the grain." "More contrast." These are creative directions, not technical specs.
Lock it down
Run the quality checklist. Check cursor-pointer, border-sub, type scale, grain presence, reduced motion support.
| What we said | What Claude did |
|---|---|
| "There are weird straight lines" | Replaced all linear-gradient + conic-gradient with radial-gradient ellipses |
| "Heavier on the black" | Added dedicated black radial-gradient layers in every mesh palette |
| "Dial up the grain" | Bumped opacity 0.18 to 0.25, baseFrequency 0.75 to 0.85, octaves 4 to 5 |
| "Hard to read the Geist Mono" | Bumped text-white/40 to /70, added font-medium, increased text-[10px] to text-[11px] |
| "Should we use toast?" | Installed Sonner, configured with brand styling, rewired all copy buttons |
| "Vertically center the numbers" | Changed items-start to items-center, removed mt-0.5 on number span |
| "Use semi-bold intelligently" | Applied font-medium selectively to mono text on dark/busy backgrounds |
Why this works: Claude holds the entire design system in context. When you say "dial up the grain," it knows the current grain opacity, the baseFrequency, and where grain is applied (page-level, mesh, video). It makes a coherent change across all surfaces. A Figma file cannot do this. A Sketch symbol cannot do this. Claude Code can.
Every design system needs a self-documenting page. Not documentation. A living specimen where every swatch is the actual token and every animation is the actual CSS class.
Structure it as numbered sections with ghost numbers, eyebrow labels, and live interactive examples. Color swatches are clickable to copy. Code snippets are copy-paste ready. Mesh gradient demos render inline. Toast notifications for copy feedback.
01 / Brand — Logo, lockup, usage rules 02 / Colors — Light mode, dark mode, accent, utility tokens 03 / Type — Font families, scale, labels 04 / Space — Spacing tokens, containers, breakpoints 05 / Components — Buttons, cards, pills, inputs 06 / Motion — Easing curves, animations, scroll reveal 07 / Layout — Section structure, grids 08 / Aurora — Palette, gradient, mesh variants, overlays 09 / Templates — Remotion video compositions, downloads 10 / Export — SKILL.md download, install commands
The mesh gradients are not just for web. Port them to Remotion. Same blob positions and colors. Use useCurrentFrame and interpolate for animation. Register compositions at 3840x2160 for each palette variant.
// Same blob math, Remotion's frame system const frame = useCurrentFrame() const cx = interpolate( Math.sin(frame * 0.003 * mult + phase), [-1, 1], [blob.cx - blob.dx, blob.cx + blob.dx] )
Video editors drop the rendered MP4 into their timeline. The brand system renders itself.
The design system becomes a Claude Code skill. When anyone invokes /design-system, Claude gains complete knowledge of every token, pattern, component, and convention.
A SKILL.md is not documentation. It is a persona injection. You tell Claude who it becomes when this skill is active.
--- name: Arctic Midnight Design System description: Build UI, video overlays, and export assets using the Arctic Midnight system. --- # Arctic Midnight Design System You are three people. You are a principal-level Brand Architect, a principal-level UX Engineer, and a principal-level Motion Designer.
Then the body: roles, tokens, typography rules, component patterns, mesh gradient docs, animation catalog, layout patterns, quality checklist.
Put the SKILL.md in your public directory. Add a download button and curl command.
mkdir -p .claude/skills/design-system curl -o .claude/skills/design-system/SKILL.md \ https://modh.ca/exports/design-system-skill.md
The entire Arctic Midnight design system, encoded as a Claude Code skill. Drop it into any project.
What we discovered building Arctic Midnight through iterative vibe coding with Claude Code.
Start with tokens, not components
If your CSS variables are right, the components practically write themselves. If your tokens are wrong, every component fights the system.
Grain makes everything premium
A flat gradient looks digital. A grainy gradient looks cinematic. The difference between "AI-generated" and "designed" is often just texture.
Use radial-gradient for organic blending
Linear gradients create bands. Conic gradients create pinwheels. Radial gradient ellipses create soft, organic color fields. Always use ellipses for more natural shapes.
Black radial gradients create depth
Layer 1-2 dedicated black radial gradients in every mesh palette. Color should emerge through darkness, not sit on top of it.
Never use font-bold
In a well-designed system, hierarchy comes from size and tracking, not weight. font-medium (500) is the maximum. Use it on headings and mono text on dark backgrounds.
Name problems, not solutions
When giving feedback to Claude, describe what feels wrong, not what to change. "Hard to read" is better than "change the opacity." Claude holds the full context.
Toast notifications are table stakes
If a user copies something, they need instant feedback. Install Sonner. Brand the toast styling. Wire every copy action.
Accessibility is not optional
prefers-reduced-motion support in every animation. aria-hidden on decorative elements. Minimum contrast ratios on text overlays. These are requirements.
The SKILL.md is the ultimate export
When you encode your design system as a Claude Code skill, anyone with Claude Code can build in your system. That is the real deliverable.