Skip to content

Skill · 02 of 09

craft look designed, not generated

The void visual system. Color, typography, layout, spacing, radius, shadows, hairlines, focus rings, texture, imagery and icons for websites and web apps…

skills/craft/SKILL.md167 lines7 referencesupdated 16 Sept 2026

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

  1. One direction per project. Import exactly one directions/.css. Never mix two directions' fonts, radii or motion.
  2. Semantic token utilities only. bg-surface, text-fg-muted, border-line, bg-brand. No hex, no bg-zinc-900, no text-[#777] in components (lint/hardcoded-colors). The default palette is removed, so raw palette classes emit nothing.
  3. 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).
  4. No pure #000 text on #fff. Neutrals are tinted (the tokens already do this). Exception: monochrome-brutal brands (hellohello, basement) and #000 canvas only when surfaces step up to ≥ L 18% with visible hairlines.
  5. 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-hidden text only, 3:1 — never text someone must read).
  6. 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 add leading-normal, tracking-normal or font-bold on top (design/heading-tracking).
  7. 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.
  8. 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.
  9. Max 2 families + 1 mono, ≤ 4 font files, next/font only (design/too-many-font-families, perf/font-budget).
  10. 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.
  11. Left-align by default. ≤ 25% of h1–h3 centered (14 of 18 sites). Center only a short hero lockup or a closing CTA.
  12. Radius comes from the --radius knob. rounded-xs … rounded-3xl. Cards ≤ rounded-xl; 16px+ only for device frames and media. Nested corners concentric (radius-nested) (design/radius-sprawl).
  13. One separation method per element: border or shadow or background shift. Shadows are shadow-1/2/3 only (layered, low alpha, ring included).
  14. Visible, double-contrast focus. The base layer ships :focus-visible { outline: 2px solid var(--line-focus); outline-offset: 2px }. Never outline-none without a replacement (lint/outline-none-no-replacement, a11y/focus-not-visible).
  15. 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-bg bg-bg-subtle bg-surface bg-surface-raised bg-surface-hover bg-surface-active; lines border-line-subtle border-line border-line-strong, focus outline-line-focus; text text-fg text-fg-muted text-fg-subtle text-fg-on-brand; brand bg-brand hover:bg-brand-hover bg-brand-subtle border-brand-line text-brand-text; status text-success bg-danger-subtle ….
  • Text on a tinted surface uses that family's text token: text-brand-text on bg-brand-subtle, never text-fg-muted.
  • Hover must raise contrast (ghost text-fg-mutedhover: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), Vercel rgba(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: balance on h1–h4 and pretty on 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: hero pt-16 pb-20 lg:pt-32 lg:pb-28, a dense proof strip py-12, a showcase py-24 lg:py-40. Identical py-20 everywhere 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-h 3.5rem default), sticky, bg-bg/92 with 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, never h-screen (lint/vh-hero). overflow-x: clip, never hidden, 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). Buttons rounded-lg, cards rounded-xl, dialogs rounded-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 compute outer = inner + padding by hand.
  • Elevation: shadow-1 resting cards (light: 1px contact; dark: inset highlight only), shadow-2 popovers/menus, shadow-3 dialogs and hero product frames. Primary buttons add shadow-(--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-subtle dividers; gap-px bg-line over a grid for 1px cell rules; hairline utility for a bordered box.
  • Mono labels: label-mono for eyebrows, table heads, metadata, kbd hints, status. Use eyebrows on ≤ 1/3 of sections.
  • Press: press utility on every pressable (scale 0.97 on :active, 100ms). Never hover:scale-105.
  • Hit areas: hit-area on targets under 24px (44px on coarse pointers) (a11y/target-size).
  • Grain: grain utility, 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: stable is 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 in motion/references/recipes.md).
  • Typography hygiene: not ..., curly quotes, en dash for ranges (9–5), non-breaking space in 10 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-subtle via SVG currentColor, 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)

References