Design Craft
Constraints + Consistency + Restraint = Quality. LLMs converge on the statistical median of every Tailwind tutorial and template — generic, safe, forgettable. This skill provides the decisions a strong designer would make, pre-made: executable kernels for type, color, spacing, and interaction, plus branch recipes for everything else.
Hard Guardrails
| Reflex |
Build instead |
| Purple/violet/indigo→pink/fuchsia gradients, gradient text, blurred gradient orbs |
One flat committed surface, real media, or a tonal treatment derived from your own scale |
| Decorative glass/glow/blur on static surfaces |
Named elevation (tint + shadow); glass only on overlays with a readable fallback |
| Centered hero + identical card grid; 3-4 equal big-number metric cards |
A content-driven composition: asymmetric anchor + supporting cells, or one prominent metric with context — [reference/composition.md](reference/composition.md) |
| Fake proof: "Trusted by 10,000+", logo marquees, "John Doe"/"Acme Corp", round metrics (99.99%), fake terminal/browser/mac-window chrome (traffic-light dots on mockups included) |
Real customers, real numbers, the real product surface without a fake window around it — or omit the section entirely |
| Colored left-border stripes on cards, callouts, event/schedule items, alerts |
Tinted surface OR a leading dot/chip/time column — one cue, never stripe + tint. Category color lives in a filled dot or label, not an edge bar (blockquote prose quotes exempt) |
| Unmodified component-library defaults (verbatim shadcn variant strings, default radii/tokens, Inter-by-reflex) |
Customize tokens, radius, and type to this product before composing pages |
Dead controls: href="#", empty onClick, toggles that don't persist |
Wire the real behavior or remove the control — a dead affordance is a broken promise |
| Attention-begging motion: pulse/ping on CTAs or pricing, identical fade-in-up on every section |
Motion communicates feedback, state, or spatial relationship, bounded to its role — [reference/motion.md](reference/motion.md) |
| Template copy: "Seamlessly", "Effortlessly", "Streamline", "Unlock", "It's not just X, it's Y", "Welcome to our platform", "Built with ❤️", "AI-powered" (non-AI product) |
The concrete claim: what the product does, for whom, in specific verbs |
The intent-and-repetition lens — for every context-sensitive treatment (warm neutrals, texture, illustration, large radii, uppercase micro-labels, sparklines, dark sections): the treatment must communicate state, hierarchy, focus, information, or brand meaning, and be bounded to a named role. One brand kicker is voice; an eyebrow above every section is scaffolding. A skeleton pulse is feedback; a pricing pulse is begging. A coherent warm neutral scale is a palette; a --cream token by reflex is the median. Token names are never the failure — unbounded repetition without purpose is.
Combination tells — individually forgivable, failing in combination (2+ together): eyebrow kickers on multiple sections, numbered section markers outside a real sequence, radius ≥24px outside a declared soft/playful direction, cards nested in cards, icon-heading-line cells repeated identically, different background color per section.
Project Context Scan (MANDATORY — before Design Decision Gate)
Determine brownfield (existing design system) or greenfield:
components.json → shadcn project, tokens in globals.css under :root
tailwind.config → theme.extend.colors custom tokens
globals.css / app/globals.css → CSS custom properties (--primary, --background, …)
package.json → UI libraries: shadcn, Radix, Panda CSS, vanilla-extract
ANY found → BROWNFIELD: catalog existing tokens and use them. Skip color derivation. Derive only genuinely missing roles, harmonized with existing hues. None → GREENFIELD: run the full Design Decision Gate including color derivation.
Design Decision Gate (MANDATORY — before writing any code)
State these decisions explicitly in your reasoning:
- Structural archetype: app shell (sidebar + content) · marketing page (sections + CTA) · focused tool (centered workspace) · dashboard (data-dense grid) · editorial (long-form reading)
- Visual density: sparse · balanced · dense
- Aesthetic direction — a specific adjective pair matched to the product, not "modern" or "clean": stark technical, bold expressive, soft organic, restrained editorial, luxury refined, playful toy-like, retro-futuristic, industrial utilitarian.
- Initial mode from the physical scene — write one sentence: who uses this, where, under what light, in what mood. That sentence picks light or dark. Design the chosen mode first; derive the other via semantic token remapping, never as a separate design.
- Typeface — from the font menu below, matched to the aesthetic (availability rule in the type kernel).
- Color strategy — BROWNFIELD: list existing tokens. GREENFIELD: run the color kernel below.
- Composition — hero architecture from the menu below (marketing) or shell recipe from [reference/composition.md](reference/composition.md) (apps, dashboards, settings, detail pages).
- The unforgettable question: "If someone saw 10 similar pages today, what would make them remember THIS one?" Name one concrete visual or structural choice.
- The swap test + category-reflex check: If swapping the layout for a template and the font for Inter would go unnoticed, those choices defaulted — remake them. If the palette or aesthetic is guessable from the product category alone (or from category-plus-anti-reference: "fintech but not navy → terminal dark"), that's the training-data reflex one tier deep; rework until neither is obvious.
The archetype and density decisions drive everything else. The delight-impact curve: rare moments (onboarding complete, first success) earn more delight than daily actions. Polish is uniform — settings, empty states, and errors get the same care as the hero.
Reference loading triggers (MANDATORY):
- Palette, contrast, or dark-mode depth → READ [reference/color.md](reference/color.md)
- Font loading, fallbacks, variable fonts, long-form text, CJK/multi-script → READ [reference/typography.md](reference/typography.md)
- Radius/elevation systems, density modes, block rhythm, safe areas, optical alignment → READ [reference/spacing.md](reference/spacing.md)
- App shells, sidebars, gutters, metadata/date lockups, settings, features, pricing, proof, footers → READ [reference/composition.md](reference/composition.md)
- Async data, forms, validation, empty/error/loading, destructive actions, optimistic updates → READ [reference/product-states.md](reference/product-states.md)
- Modals, drawers, transitions, gestures, any interactive motion → READ [reference/motion.md](reference/motion.md)
- Dashboards, tables, charts, KPIs, financial UI → READ [reference/data-dense.md](reference/data-dense.md)
- Icons, images, illustration, emoji → READ [reference/visual-assets.md](reference/visual-assets.md)
Hero Menu (marketing pages)
| Architecture |
Layout |
Use when |
| Splitscreen |
55/45 or 60/40 text + media, asymmetric |
Product with strong visuals |
| Editorial |
Oversized display heading, minimal supporting text, no image |
Brand/statement pages |
| Full-bleed media |
Viewport-height image/video with text overlay |
Visual-first brands, portfolios |
| Asymmetric mosaic |
Unequal grid cells with mixed media + text |
Creative agencies, portfolios |
| Anchored metric |
One hero stat with context paragraph |
SaaS, data products |
Hero rules: spacing min py-24 md:py-32; headline sizes from the type kernel with a clamp() ceiling of 6rem — a larger ceiling is allowed only when visual scale IS the feature and the live render proves it survives every breakpoint. Subtext max 2 sentences, max-w-[50ch]. One CTA; secondary action is a text link. Real imagery with alt text, or bold typography as the visual — never colored boxes or icon grids. Heading copy must survive every breakpoint.
Type Kernel
Five roles with default sizes — adjust deliberately per aesthetic, never by reflex: Display clamp(2.5rem, 1.5rem + 4vw, 6rem) (hero statements) · Heading 1.5–2.25rem (section/page titles) · Body 1rem (reading text) · Caption 0.875rem (supporting meta) · Micro 0.75rem (labels, badges). Fewer sizes with more contrast beat many close sizes — app UI rarely needs more than these five plus one intermediate heading step.
| Rule |
Value |
| Tracking |
≥48px: -0.02em · 30–47px: -0.01em · <30px: 0 · uppercase micro-labels only: +0.05em. After rendering, adjust at most ±0.005em for the chosen face. Hard floor -0.03em; tracking-tighter fails. |
| Line height |
Display 1.0–1.1 · Heading 1.2–1.3 · Body 1.5–1.6 · short UI labels 1.0–1.2. Unitless. |
| Case |
Sentence case for headings, labels, tabs, buttons. ALL CAPS only for micro-meta ≤3 words. |
| Numerals |
tabular-nums + lining figures on all data — prices, counts, dates, IDs, table columns. Right-align numeric columns. |
| Wrapping |
Headings text-balance, body text-pretty, measure max-w-[65ch]. |
| Weights |
Max 3 per view; minimum range 400/500/600 — two-weight interfaces lack nuance. |
| Mobile inputs |
≥16px or iOS zooms. Never user-scalable=no. |
| Availability |
Verify the font's source/license/package before selecting. System font stacks are valid for brownfield and performance-first UI. |
Font menu by aesthetic (open menu — any verified font that matches the direction qualifies):
| Aesthetic |
Examples |
| Editorial |
Instrument Sans, Source Sans 3, Libre Franklin |
| Technical / stark |
Geist, IBM Plex Sans, Commit Mono (accents) |
| Friendly / soft |
Plus Jakarta Sans, Nunito Sans, Outfit |
| Bold / expressive |
Sora, Space Grotesk, Clash Display (display only) |
| Neutral / professional |
General Sans, Satoshi, Switzer |
Reaching for Inter/Roboto/DM Sans "just to pick something" is defaulting — return to the aesthetic direction. One family unless genuine display/body contrast (serif+sans, geometric+humanist). Monospace as small accent (font-mono text-xs on IDs, timestamps) creates productive contrast in technical products; monospace display headings do not.
Loading strategy, fallback metrics, variable fonts, CJK: [reference/typography.md](reference/typography.md).
Color Kernel
Strategy axis — state before picking colors: Restrained (tinted neutrals + one accent ≤10%, default for tools/dashboards, chroma ×0.6) · Committed (one saturated color carries 30–60%, marketing, ×1.0) · Full palette (3-4 named roles, campaigns/data viz, ×1.2) · Drenched (the surface IS the color, heroes, ×1.4). The multiplier scales every stop's chroma; re-clamp against the gamut caps after scaling. "Restrained by reflex" is the same failure as "Inter by reflex."
GREENFIELD derivation (executable, in order):
- Hue from the product name:
H = (firstletterordinal × 137 + secondletterordinal × 47) mod 360 (a=1…z=26; skip non-letters, and if fewer than two letters exist, reuse the first). Never replace with a category stereotype. If the derived hue cannot meet contrast/gamut after capping, shift ±30° and re-derive. A solid committed surface at the derived hue is always legal — the gradient guardrail bans gradient treatments, not hues.
- 12-stop lightness spine at the derived hue:
L = 0.99, 0.96, 0.90, 0.82, 0.71, 0.64, 0.55, 0.49, 0.40, 0.32, 0.27, 0.24 (stops 50…950). Committed-strategy chroma per stop: C = 0.02, 0.04, 0.06, 0.09, 0.11, 0.13, 0.13, 0.12, 0.10, 0.08, 0.06, 0.05 — scale by the strategy multiplier, then cap C at 0.09 for hues 170–210° and ~0.18 elsewhere (sRGB safety).
- Companion neutral scale at
H+180° (≥120° separation), C ≤ 0.02 — the tinted neutrals that replace dead white/gray. Never #fff/#000 as surfaces; use stop-50/stop-100.
- Map to semantic tokens:
--background --foreground --card --primary --secondary --muted --accent --destructive --border --ring. Components use ONLY semantic tokens — never bg-blue-500, never inline hex/rgb/oklch in markup. Opacity modifiers on tokens are allowed (bg-primary/10).
- Contrast floors (WCAG 2.x conformance): 4.5:1 normal text, 3:1 large text, 3:1 for required non-text UI (borders of inputs, icons carrying meaning, focus rings) — measured against the actual rendered background, placeholders included. Fix failures by moving L, not C. Every status conveys through a non-color cue too (icon, text, weight).
- Dark mode is a remap: invert the semantic lightness mapping onto tonal surfaces (lighter = closer), reduce chroma ~10–20%, prefer surface tint over shadow, weight 350 only when the loaded font provides it. Swap the semantic layer, not components.
Universal status colors — success=green, error/destructive=red, warning=amber, info=blue. Users already know these; never invert, never use brand accent for status.
60-30-10: 60% neutrals, 30% secondary, 10% accent; one accent at 100/60/20/10% opacity before adding a second. All section backgrounds come from the same neutral scale, and a landing page is one continuous canvas: at most one emphasis panel + the footer change surface, no full-width rules between sections — variety comes from density, typography, and rhythm. Token names reveal the product (--meridian-600, not --gray-700).
Gamut mapping, P3, warm-neutral branch, APCA (supplementary only), dark-mode depth: [reference/color.md](reference/color.md).
Spacing Kernel
4px grid — all spacing in multiples of 4.
tight: 4px — within atomic elements
item: 8px — between items in a group
group: 16px — between related groups
section: 24–32px — between major sections
page: 32–64px — page-level margins
| Rule |
Value |
| Inset vs stack |
Inset = padding inside a component; stack = gap between siblings. Set them independently — equal inset+stack everywhere is how "same padding everywhere" happens. |
| Rhythm |
Tight within groups, larger between groups, varied section density. |
| Grouping cue |
Pick ONE per region: spacing, border, or elevation — never all three. |
| Surfaces |
Max 3 visible surface levels (page → section → card). |
| Radius |
One base radius, derive the rest; concentric nesting: outer = inner + padding. Scale values and aesthetic exceptions: [reference/spacing.md](reference/spacing.md). |
| Targets |
Mobile web ≥44×44 CSS px (WCAG AA floor is 24px; Android-native prefers 48dp). Extend with pseudo-elements when the visible element is smaller. Adjacent hit areas never overlap. |
| Tokens |
Framework scale only — never p-[13px], gap-[7px]. Acceptable arbitrary: max-w-[65ch], min-h-[*rem], grid templates. |
| Height |
Comes from content + the ladder, never from min-height + vertical centering (sole exception: a full-viewport hero where scale is the declared feature). Empty bands between blocks within one surface: ≤64px app/data, ≤128px marketing. At section boundaries stacked paddings may total ~192px on marketing pages; >256px is dead air anywhere. Larger = unowned gap: pull the next block up or give the band content. |
Density modes, block text rhythm, elevation ladder, shadows, safe areas, optical alignment: [reference/spacing.md](reference/spacing.md). Shell geometry, gutters, lockups: [reference/composition.md](reference/composition.md).
Layout Mechanics
| Rule |
Detail |
| Structure |
Flexbox for 1D, Grid for 2D. Never absolute positioning for structural layout. |
| Viewport |
h-dvh, never h-screen (iOS Safari). |
| Overflow |
min-w-0 on flex/grid children with text. overflow: hidden is banned as layout repair — fix the content or the container. |
| Z-index |
Fixed semantic scale (dropdown → sticky → backdrop → modal → toast → tooltip), never z-[999]. |
| Overlays |
Dropdowns/tooltips inside overflow: hidden/auto ancestors get clipped — use the Popover API, position: fixed, or a portal. |
| Sizing contract |
Declare per element: Hug (wraps content), Fill (flex: 1), or Fixed (sidebars, avatars). |
| Grids |
No empty cells: remainder 1 → last item spans the row; remainder 2 → both span half. auto-fill + minmax(280px, 1fr) for variable counts. |
| Semantic HTML |
<button> for actions, <a> for navigation — never <div onClick>. Icon-only controls get aria-label. |
| Responsive |
2-tier default (mobile + desktop). @container queries when the same component serves different-width regions. Never hide core functionality on mobile. Test extremes: 100+ char strings, emoji, RTL, empty, 1000+ items. |
| Asymmetry |
grid-cols-[2fr_1fr] beats equal columns; vary section density; whitespace is composition — consistent and framed, not accidental. |
Structural completeness: an app shell has navigation context + full-height content (a form in a void is missing structure); a dashboard fills its grid; a detail page pairs primary content with supporting context. Complete recipes: [reference/composition.md](reference/composition.md).
Sidebar: same hue family as content, 1–2 lightness steps apart — no chroma cliff between shell and content. Inactive items recede (reduced opacity), active state is tonal (no side-stripe). Content always wins the hierarchy contest. Full anatomy: [reference/composition.md](reference/composition.md).
Interaction & Completeness Kernel
Every interactive element has all applicable states: default, hover (gated @media (hover: hover)), focus-visible (visible ring — never removed), active (pressed feedback), disabled. Every async action runs the lifecycle idle → pending → success/error: pending disables re-activation and shows status (aria-busy), success/error is visible where the action happened.
| Rule |
Detail |
| Async data surfaces |
Design all four: loading, populated, empty, error — before shipping any of them. Recipes: [reference/product-states.md](reference/product-states.md). |
| Empty states |
Fact + context, plus a next action when one truly exists. |
| Errors |
Inline, adjacent to the cause: what happened, why, how to fix. No jargon, no humor. |
| Destructive actions |
Reversible → immediate + undo window. Irreversible/high-stakes → explicit confirmation naming what and how much. |
| Optimistic UI |
Safe reversible actions only; visible rollback on failure — never payments or deletions. |
| Loading |
Structural skeletons matching content shape, not generic spinners. |
| Buttons |
Label never wraps (white-space: nowrap) — shorten the label or widen the button. Verb+object: "Save changes", not "OK". |
| Primitives |
Accessible primitives (shadcn/Radix/Base UI) customized to the project; never rebuild keyboard/focus behavior; never mix systems. |
| Formatting |
Intl.NumberFormat/Intl.DateTimeFormat, never string concatenation. Never block paste. |
UX Writing
Specific verb + object on buttons. One term per concept (Delete/Remove → pick one). Active voice. Cut every sentence in half, then again. Budget +30–40% width for i18n. Errors: never humor, never raw jargon (500, ECONNREFUSED). Never restate the heading in an intro.
Symptom → Correction
| Symptom |
Likely cause |
Fix |
| Decent but generic |
Equal-weight cards, uniform padding |
Remove a widget, merge a region, flatten one surface |
| Everything competes |
Multiple primary-weight elements |
ONE dominant element, mute the rest |
| Cramped |
Uniform tight spacing |
Increase section gaps, keep group spacing tight |
| Empty despite content |
Oversized gaps, thin type |
Tighten item spacing, add weight to key elements |
| Floaty content islands |
min-height + centering, stacked margins, uniform huge section gaps |
Height from content; snap every gap to the ladder; kill voids over the Height rule's band limits |
| Ragged two-column section |
Heading, controls, and aside start at different y |
One section origin: heading row baseline-aligned, both columns start one group unit below it |
| Clips on real content |
Fixed heights |
min-h + overflow-y-auto on scroll regions |
| Dark mode washed out |
Same chroma/weight as light |
Reduce chroma ~10–20%, reduce weight where the font allows |
| Numbers jump on update |
Missing tabular-nums |
font-variant-numeric: tabular-nums |
| Sidebar fights content |
Too bright, equal weight |
Dim shell 1–2 steps, inactive items to reduced opacity |
Conflict Priority
Sacrifice in this order (last = drop first): 1. functional integrity → 2. readability → 3. spatial rhythm → 4. motion polish → 5. decorative novelty.
Self-Check (MANDATORY — before finishing any UI work)
Re-read every line you wrote. Verify each item; fix failures before responding.
- Guardrails — zero hard-guardrail patterns; combination tells don't stack.
- Tokens only — every color is a semantic token; zero inline hex/rgb/oklch in markup; zero arbitrary spacing/type values.
- Type kernel — tracking table honored,
tracking-tighter absent, one h1, heading levels don't skip.
- States complete — every interactive element has its five states; every async surface has loading/populated/empty/error; every async action blocks duplicate activation.
- Layout mechanics —
h-dvh, min-w-0 where text can overflow, no empty grid cells, no wrapped button labels.
- The swap test — could the font become Inter and the layout a centered template without anyone noticing? If yes, remake those decisions.
- References honored — name which reference files you read this session. If a mandatory trigger fired and its file went unread, read it now and re-verify the affected code.
When the work claims done, run the design-qa gates (scanner + live render).