When Claude Code loads it
Use when styling or restyling any page, section or component; when choosing fonts, colors, a brand accent, dark mode, spacing or shadows; when a UI looks generic, "AI-generated", cheap, flat, cluttered or template-like; when adding Tailwind classes, theme tokens or a custom brand color; when reviewing screenshots for visual quality; or when `void lint` / `void design` report design/* or lint/tw-* findings.
Every rule here comes from measured teardowns of 24 top sites (Linear, Vercel, Stripe, Resend, Granola, hellohello, …) or from bugs in shipped apps.
Workflow lives in website. Motion in motion. Performance in speed. Accessibility in a11y.
The 15 non-negotiables
- One direction per project. Import exactly one
directions/. Never mix two directions' fonts, radii or motion..css - Semantic token utilities only.
bg-surface,text-fg-muted,border-line,bg-brand. No hex, nobg-zinc-900, notext-[#777]in components (lint/hardcoded-colors). The default palette is removed, so raw palette classes emit nothing. - One accent, rationed to 1–5% of the page. Brand color only on: the primary CTA, links, focus, the active state, selection, and at most one highlight. Measured: Vercel's blue on 2 elements, Anthropic's clay on 1 (
design/multiple-accents). - No pure
#000text on#fff. Neutrals are tinted (the tokens already do this). Exception: monochrome-brutal brands (hellohello, basement) and#000canvas only when surfaces step up to ≥ L 18% with visible hairlines. - Four text tiers, no more:
text-fg(headings, body),text-fg-muted(secondary),text-fg-subtle(meta, captions, placeholders — AA 4.5:1, never paragraphs),text-fg-faint(disabled controls, decorative/aria-hiddentext only, 3:1 — never text someone must read). - Display type is tight.
text-display*tokens bake line-height 0.92–1.04 and negative tracking (−0.028 to −0.045em × direction knob). Never addleading-normal,tracking-normalorfont-boldon top (design/heading-tracking). - Display weight 400–600. 18 of 23 measured H1s are ≤ 600. 700 is allowed only when the direction sets it (
swiss) or the brand face is built for it. - Workhorse UI text is 13px (
text-sm); body is 15–16px (text-base/text-md); long reads 17–19px (text-lg). Real sites run on 13–14px UI + 12px mono texture, not 16px everywhere. - Max 2 families + 1 mono, ≤ 4 font files,
next/fontonly (design/too-many-font-families,perf/font-budget). - 4px spacing grid. Allowed steps: 1 2 3 4 5 6 8 10 12 16 20 24 32 40 (×4px). No
p-[13px](design/spacing-off-grid). Space between groups ≥ 2× space within a group. - Left-align by default. ≤ 25% of h1–h3 centered (14 of 18 sites). Center only a short hero lockup or a closing CTA.
- Radius comes from the
--radiusknob.rounded-xs … rounded-3xl. Cards ≤rounded-xl; 16px+ only for device frames and media. Nested corners concentric (radius-nested) (design/radius-sprawl). - One separation method per element: border or shadow or background shift. Shadows are
shadow-1/2/3only (layered, low alpha, ring included). - Visible, double-contrast focus. The base layer ships
:focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px }. Neveroutline-nonewithout a replacement (lint/outline-none-no-replacement,a11y/focus-not-visible). - Run the anti-slop checklist (below) on screenshots at 1440 and 390 before calling anything done.
1. Direction
Directions live in packages/tokens/css/directions/ (copied into a project at src/styles/void/directions/). Each sets fonts, radius, display weight/tracking, durations, and light + dark color roles.
| Direction | Pick for | Scheme | Fonts | Radius | Feel |
|---|---|---|---|---|---|
precision |
dev tools, AI/infra, pro SaaS, technical launches | dark | Geist + Geist Mono | 6px | Linear/Vercel: hairlines, mono labels, indigo signal |
editorial |
essays, research, studios, premium brands, long docs | light | Newsreader + IBM Plex Mono | 2px | paper, ink, oxblood red pencil, rules not boxes |
swiss |
agencies, architecture, events, portfolios, data journalism | light | Schibsted Grotesk + JetBrains Mono | 0 | poster grid, black rules, signal red |
warm |
consumer SaaS, notes, education, health, community | light | Bricolage Grotesque + Figtree | 10px | notebook app, clay accent, soft warm shadows |
terminal |
CLIs, infra, hacker tools, dev launch pages | dark | Geist Mono only | 0 | graphite, amber phosphor, hard edges |
instrument |
dashboards, admin, finance, ops, data docs | system | IBM Plex Sans + Plex Mono | 6px | dense, tabular, chart-grade blue |
Unsure and the product is technical: precision. Unsure and it is content: editorial. Full specs, next/font snippets, signature details and example heroes: references/directions.md.
Switching direction = 3 edits that must agree: the @import in src/app/globals.css, the next/font loaders in src/app/layout.tsx (variable names are in the direction file header), and direction in src/site.config.ts.
2. Color
- Components reference roles: surfaces
bg-bgbg-bg-subtlebg-surfacebg-surface-raisedbg-surface-hoverbg-surface-active; linesborder-line-subtleborder-lineborder-line-strong, focusoutline-line-focus; texttext-fgtext-fg-mutedtext-fg-subtletext-fg-on-brand; brandbg-brandhover:bg-brand-hoverbg-brand-subtleborder-brand-linetext-brand-text; statustext-successbg-danger-subtle…. - Text on a tinted surface uses that family's text token:
text-brand-textonbg-brand-subtle, nevertext-fg-muted. - Hover must raise contrast (ghost
text-fg-muted→hover:text-fg), never lower it. - Lines are alpha of the foreground (7–17%), so they work on every surface. Measured: Linear
rgba(255,255,255,.08), Vercelrgba(0,0,0,.08). - Dark mode is a separate scale, not an inversion: bg L 14–18%, elevated surfaces lighter (+3% L per level), text L ≈ 96%, brand solid unchanged. The direction files already do this. Never write
dark:bg-…on semantic tokens. - Status colors are for status only, always with an icon or text label.
- Custom brand hex from the user:
references/color.md(OKLCH procedure +packages/tokens/scripts/generate.ts scale). - Selection is brand-tinted (base layer). Set
per scheme (design/theme-color-missing,design/color-scheme-missing).
3. Typography
| Role | Token / classes | Measured basis |
|---|---|---|
| Hero H1 | text-display-lg (42→80px, lh 0.96) or text-display-xl (46→96px) + max-w-[14ch]–max-w-[20ch] |
H1 46–96px desktop, 28–48px mobile; lh 0.85–1.15 |
| Section H2 | text-display-sm (34→54px) or text-4xl (30→44px) |
H2 32–56px |
| H3 / card title | text-xl or text-2xl, font-medium |
20–24px |
| Lead / hero sub | text-lg text-fg-muted max-w-[44ch] |
18–24px, max-width 480–672px |
| Body | text-base (15px) product, text-md (16px) marketing, text-lg long-form |
16px/1.5; long reads 1.625–1.75 |
| UI, nav, buttons | text-sm (13px), font-medium |
most frequent style = 14px |
| Label / eyebrow | label-mono text-fg-muted (11px, uppercase, +0.06–0.12em) |
mono labels +0.1 to +0.15em |
| Numbers | tabular-nums (auto on table/time/output/kbd) |
Linear: 65 tabular declarations |
- Tracking tightens with size; the text tokens carry it. Never set positive tracking on lowercase body.
text-wrap: balanceon h1–h4 andprettyon paragraphs are in the base layer (design/heading-no-balance).- Headline widths in
ch: 10–20ch for H1, ≤ 28ch for H2. Paragraph measure 45–72ch (design/line-length-long). - Two-tone headline (Linear, Stripe):
.First phrase. continuation that explains.
- No single serif-italic word inside a sans headline (a 2025–26 template tell). Commit to the whole line or none.
- Tables of pairings, OpenType features, fluid clamps:
references/typography.md.
4. Layout and spacing
- Page wrapper:
container-page(76rem, fluid gutter 20→40px). Reading column:max-w-prose mx-auto(65ch). Wide/dashboard:max-w-wide. - Section rhythm:
section-y(64→128px). Vary it on purpose: heropt-16 pb-20 lg:pt-32 lg:pb-28, a dense proof strippy-12, a showcasepy-24 lg:py-40. Identicalpy-20everywhere is a tell. Measured desktop 96–160px, mobile 60–96px. - Grids: 12 columns with asymmetric spans (7/5, 8/4, 3/9). Avoid 4/4/4 rows of identical cards.
- Header 56–72px (
--header-h3.5rem default), sticky,bg-bg/92with an optional blur. Blur only on the header. - Heading-to-content gap is smaller than the gap above the heading (1:2 to 1:3).
- Full-height heroes use
min-h-svh, neverh-screen(lint/vh-hero).overflow-x: clip, neverhidden, on html/body (lint/overflow-x-hidden-sticky). No horizontal overflow at 390px (design/horizontal-overflow). - Grid recipes, container queries, density table:
references/layout.md.
5. Depth
- Radius knob per direction; utilities derive:
rounded-xs(×0.25)sm(×0.5)md(×0.75)lg(×1)xl(×1.5)2xl(×2)3xl(×3). Buttonsrounded-lg, cardsrounded-xl, dialogsrounded-2xl. Pill only if the direction or brand picks pills for CTAs, and then consistently. - Nested corners: parent
radius-nested(child radius = parent − padding), or computeouter = inner + paddingby hand. - Elevation:
shadow-1resting cards (light: 1px contact; dark: inset highlight only),shadow-2popovers/menus,shadow-3dialogs and hero product frames. Primary buttons addshadow-(--elevation-highlight)(inset top light, bottom shade). - Dark depth = lighter surface + 1px alpha ring + inset top highlight, not bigger black shadows.
surface-card= surface + hairline +rounded-xl+shadow-1. Do not add another shadow to it.- Recipes (Linear/Vercel/Stripe shadow stacks, product frames, hairline grids):
references/depth-and-detail.md.
6. Details that read as expensive
- Hairlines:
border-line-subtledividers;gap-px bg-lineover a grid for 1px cell rules;hairlineutility for a bordered box. - Mono labels:
label-monofor eyebrows, table heads, metadata, kbd hints, status. Use eyebrows on ≤ 1/3 of sections. - Press:
pressutility on every pressable (scale 0.97 on:active, 100ms). Neverhover:scale-105. - Hit areas:
hit-areaon targets under 24px (44px on coarse pointers) (a11y/target-size). - Grain:
grainutility, opacity from the direction (0–0.06). On near-black keep it ≤ 0.02–0.035. One grain layer per page region. - One light source: at most one low-alpha same-hue radial gradient (
color-mix(in oklch, var(--brand) 10–14%, transparent)), anchored at an edge. No multi-color blobs. - Scrollbar:
scrollbar-gutter: stableis in base. Thin styled scrollbars only on inner scroll areas:[scrollbar-width:thin] [scrollbar-color:var(--line-strong)_transparent]. - Theme switch: blocking head script + transitions frozen for one frame (template
src/lib/theme.ts,components/theme-toggle.tsx; recipe inmotion/references/recipes.md). - Typography hygiene:
…not..., curly quotes, en dash for ranges (9–5), non-breaking space in10 MB.
7. Imagery and icons
- Show the product, a real artifact, or type. Best heroes: real product UI (Linear, Cursor, Attio), an art-directed asset (Stripe ribbon, Railway pixel art), or typography alone (Granola, Dia, hellohello). Never a stock photo of people at laptops, a gradient blob, or a 3D abstract render as filler.
- Product screenshots: real UI at real scale, framed with
rounded-xl border border-line shadow-3 overflow-clip bg-surface. No fake macOS traffic lights, no browser chrome, no glow halo. A hand-built DOM mock beats a PNG (sharp, themable, indexable) if it matches the real product. - Icons: one set (lucide), one stroke width (1.5), sized to the text: 16px in UI, 20px max in marketing.
text-fg-muted, inline with labels. No icons in colored circles, no emoji as icons (design/emoji-icons). - Logos of customers: only real, permitted ones; monochrome
text-fg-subtlevia SVGcurrentColor, equal optical area, 4–8 max. - Images: AVIF/WebP via
next/image, explicit size, only the LCP image eager (speed). Dark mode: dim non-photo art slightly, provide dark variants of diagrams. - No placeholder art in shipped work. Missing asset → a clearly marked TODO box in dev, removed or replaced before done (see
website§3).
8. Anti-slop checklist (30 tells → fix)
Check every screenshot against this. Before/after code for each: references/anti-slop.md.
| # | Tell | Fix |
|---|---|---|
| 1 | Purple→blue/pink gradient on hero, buttons or text | Flat bg-brand; one same-hue light at ≤ 14% |
| 2 | Gradient headline text (bg-clip-text text-transparent) |
Solid text-fg; two-tone with text-fg-muted span (design/gradient-text) |
| 3 | Several saturated accents (green badge, purple icon, orange tag) | One brand hue; icons text-fg-muted; status colors only for status |
| 4 | Pure #000/#fff with default gray-500 secondary | Direction tokens (tinted neutrals, 3 text tiers) |
| 5 | border-gray-200 solid lines everywhere |
Alpha border-line-subtle; fewer borders, more spacing |
| 6 | Glassmorphism cards over blobs | Solid bg-surface; blur only on the sticky header |
| 7 | Blurred color orbs / radial glow as the only hero visual | Product UI, real asset, or type; one edge light max |
| 8 | Default blue selection and browser focus ring | Base layer selection + line-focus ring (don't override) |
| 9 | Inter/system font at default tracking for 48px+ headings | Direction display font + text-display* tokens |
| 10 | font-bold/font-extrabold H1 |
Remove; direction weight (400–600) applies |
| 11 | Display line-height 1.2–1.5 | text-display* (0.92–1.04); never leading-normal on display |
| 12 | Everything 16px+, no small type texture | text-sm UI, label-mono metadata |
| 13 | Hero sub wider than 700px, 3+ lines | max-w-[44ch], ≤ 2 lines |
| 14 | Orphans in headings | Base text-wrap: balance; max-w-[Nch] |
| 15 | 3+ font families or random Google fonts | Direction pair + mono only |
| 16 | Centered everything | Left-align; center only hero lockup or final CTA |
| 17 | Hero → 3 icon cards → testimonials → CTA template | Section archetypes (website/references/section-archetypes.md) |
| 18 | Row of 3 identical cards with icon-in-circle + title + 2 lines | Feature deep-dive rows, editorial list with hairlines, real UI |
| 19 | Bento with random spans and no content logic | Spans by importance; each tile shows real product or data |
| 20 | Uniform py-16/py-20 sections |
section-y + deliberate variation (96–160 desktop) |
| 21 | max-w-7xl text lines of 120+ characters |
max-w-prose / max-w-[60ch] |
| 22 | 80px navbar with gradient "Get Started" | 56–64px sticky header, solid neutral or brand CTA size="sm" |
| 23 | rounded-2xl shadow-xl on everything |
rounded-lg/xl + shadow-1, or border only |
| 24 | Border + shadow + bg change on one card | One separation method |
| 25 | Non-concentric nested corners | radius-nested |
| 26 | Emoji feature icons, or icons in tinted circles | One lucide set, 16px, inline, muted |
| 27 | hover:scale-105 buttons/cards, gradient buttons with glow |
press (scale 0.97 on active); hover = color/border step |
| 28 | Fake logos, 5-star testimonial carousel, "10K+ users · 99.9% uptime" | Real proof only, or omit the section |
| 29 | Every section fades up 40px on scroll; transition-all duration-300 |
motion: 1–3 reveals, reveal utility, explicit properties |
| 30 | Buzzword copy ("Unlock", "Supercharge", "Seamless", "Elevate") | Concrete claim with a number or a named outcome (website/references/copy-and-content.md) |
Tailwind v4 traps (silent failures)
duration-(--duration-fast), neverduration-[--duration-fast](lint/tw-v3-arbitrary-var).- Undefined utilities emit nothing:
bg-primary,shadow-lg,text-5xl,bg-zinc-800are removed in void tokens (lint/tw-unknown-class). Useshadow-1..3,text-display*, semantic colors. - JS
tailwind.config.*is ignored (lint/tw-js-config-ignored).dark:needs the@custom-variantfrom base.css (lint/tw-dark-variant-os). - v3→v4 renames:
shadow-sm→shadow-xs,rounded-sm→rounded-xs,outline-none→outline-hidden,bg-gradient-to-*→bg-linear-to-*,ring= 1px (lint/tw-v3-renamed). - Full list and fixes:
references/tailwind-v4.md.
References
references/directions.md— six directions: mood, fonts (next/font), token file, signature details, don'ts, hero markupreferences/typography.md— pairings, measured tracking/leading table, features, fluid sizesreferences/color.md— OKLCH roles, dark mode, custom brand accent procedurereferences/layout.md— containers, grids, rhythm, density, responsive rulesreferences/depth-and-detail.md— radius, shadows, frames, hairlines, focus, grain, scrollbarsreferences/anti-slop.md— all 30 tells with before/after snippetsreferences/tailwind-v4.md— every silent failure and the v4 form