Design Craft
Two layers — never confuse them:
| Layer | What | Where decided |
|---|---|---|
| Creative layer | Palette, type, nav, signature element — different per project | Root DESIGN.md, generated from DESIGN-TEMPLATE.md |
| Usability floor | Evidence-backed rules that never bend | Bottom of this file |
Sibling files (same folder):
AVOID-LIST.md— versioned AI-tell list. Consult during identity generation and every design review.DESIGN-TEMPLATE.md— template + filled example for the generated per-project rootDESIGN.md.
The contract: root DESIGN.md is the project's design identity. Re-read it before every UI generation — never work from memory of it. If it doesn't exist and UI is about to be written, generate it first (two-pass process below → template). For UI projects it is the first plan task (design-reviewer authors it before any UI work).
Meta-Rule
Defaults are fine when chosen for the brief. Never when inherited.
Inter, indigo-500, rounded-lg, a 3-card feature row — each is legitimate when the brief calls for it, and slop when it appears because it's the training-data median. Every value in the UI must trace to a DESIGN.md token chosen for THIS project.
Two-Pass Process (mandatory before any UI code)
Pass A — compact token plan. Ground it in the subject's actual world — materials, place, era, mood. (A restaurant supply marketplace lives among stainless steel, wet markets, delivery bikes — not "warm and friendly".)
- 4-6 named hex colors incl. hue-biased neutrals (never pure gray)
- Display / body / utility typeface roles + weights
- One-sentence layout concept
- ONE signature element — the single bold, memorable move (distinctive hero treatment, unusual data display, strong typographic motif). One. Everything else stays quiet.
Pass B — self-critique. Ask: "Would I produce this exact plan for any similar brief?" If yes → revise and state what changed. Check the plan against AVOID-LIST.md.
Only after Pass B survives: write the plan into DESIGN.md and code — deriving every color/type/radius/motion value from it. No raw Tailwind palette colors (indigo-500) that bypass the tokens.
Navigation Selection Matrix
| Project type | Navigation pattern |
|---|---|
| Marketing / landing | Visible top bar, 4-7 text links |
| Dashboard / app | Left sidebar with labeled icons |
| Content / docs | Top bar + secondary sidebar / table of contents |
| E-commerce | Top bar + mega menu for categories |
| Peer views (same-level sections) | Tabs |
Hard rules, all patterns:
- Never a hamburger on desktop — hiding nav halves discoverability. Visible links at
md+ always. - Nav items are real links (
<a>/<Link>), never divs with onClick. - URL reflects state — views are shareable and back-button-safe.
Logo Rule — vary expression, not placement
- Top-left, clickable to home — always, for apps and sites (NNGroup: returning home is ~6x harder with a centered logo).
- Brand variety comes from wordmark treatment (typeface, weight, spacing), a motif, or an accent system — not from moving the logo.
- Centered logo ONLY for editorial/luxury briefs that explicitly call for it, and only with a compensating home affordance (visible "Home" link).
Icon Discipline
- Exactly ONE library per project, declared in DESIGN.md: Lucide / Phosphor / Heroicons / Tabler — plus stroke weight and size grid (e.g. 16/20/24).
- Every icon gets a text label — except true universals (search, close ×, chevron).
- Icon-only buttons require
aria-label. - Never emoji as UI icons.
- "No icons" is a valid choice — often the stronger one for editorial identities.
- Icon-tile-above-heading cards are the #1 AI tell (see AVOID-LIST). Icons support content; they don't headline it.
Typography Pairing Recipe
Formula: characterful display + complementary body + optional mono/data face.
- Inter / Geist are body/utility faces ONLY — never the personality carrier.
- Use weight extremes: pair 100-200 against 800-900 instead of everything at 400-600.
- Hero: 3x+ size jump from body. Scale ratio >= 1.25. Body measure 65-75ch.
tabular-numson any column of numbers. - Category starting points — rotate; never converge across projects:
- Editorial / food / lifestyle → Fraunces, Newsreader
- Technical / data → IBM Plex family
- Startup / product → Cabinet Grotesk, Satoshi These are prompts, not defaults. Pick for the brief; if the last project used it, pick differently.
- Thai: IBM Plex Sans Thai / Noto Sans Thai / Anuphan / Sarabun. Next.js: load via
next/font(self-host, no layout shift) — never a raw Google Fonts<link>.
Type scale (Tailwind)
Display / Hero: text-4xl / text-5xl+ display face tracking-tight Page Title: text-2xl font-semibold (24px) Section Title: text-lg font-medium (18px) Card Title: text-base font-medium (16px) Body: text-sm font-normal (14px, line-height 1.5-1.6) Small / Caption: text-xs text-muted-foreground (12px)
Rules: headings never font-normal · body regular, never bold-everything · max 3 sizes per component · no long ALL-CAPS runs.
Color
- Dominant + sharp accent — commit to a dominant character, cut it with one sharp accent. Timid even distribution reads generic.
- Semantic status colors (good / warn / critical) are separate tokens from the brand accent — never reuse the accent for status.
- Design light AND dark token-first; both stated in DESIGN.md. Dark mode is its own palette, not inverted gray.
- Palette sourced from the subject's world or a named cultural aesthetic — name the source in DESIGN.md.
- Neutrals are hue-biased toward the palette, never pure
#808080. No pure#000text. Contrast passes WCAG AA in both modes. - Wire tokens as CSS custom properties (Tailwind 4
@theme/ shadcn variables):--background --foreground --muted-foreground --border --primary --destructivemap to DESIGN.md tokens. - No gradients on primary buttons — solid reads intentional.
Copy Rules
- Active voice. No buzzwords (streamline, empower, supercharge, unleash).
- Buttons say exactly what happens: "Save invoice", not "Continue" / "Submit".
- Errors state the fix: "Card declined — try another card", never an apology or a stack dump.
- Declare a case system in DESIGN.md (e.g. sentence case everywhere) and keep it.
- Real content stance: realistic domain data, no Lorem ipsum, no "Welcome back, User! 👋". Copy follows the project language (Thai = real Thai copy, not stiff translation).
Density & Spacing
Layout reflects the real work, not an empty template:
- Dashboard / admin / data → dense: tables, stat rows, real data. Not "3 floating cards + giant hero".
- Marketing / landing → narrative sections with substance; hero allowed but earns its space.
- Form / settings → vertical flow, related fields grouped.
- Mix composition: table / list / stat / section — never
Card → Card → Card. Don't center everything.
Spacing scale (Tailwind — use consistently)
2 (8px) inline / icon gap 3 (12px) gap within a component 4 (16px) standard gap between elements 6 (24px) between components / card padding 8 (32px) between sections 12 (48px) major page sections
Same level = same gap. No random p-3 here, p-5 there.
Border-radius — vary by element, cap declared in DESIGN.md
Inputs / small buttons: rounded-md (6px) Cards / dialogs: rounded-lg / rounded-xl (8-12px) Avatars / pills / badge: rounded-full Cap: 12px (rounded-xl) for general containers
Component Patterns (structural — visual character comes from DESIGN.md tokens)
React 19: ref is a normal prop — no forwardRef.
tsx// Card — thin border + light shadow; no gradient border, no glassmorphism, no rounded-3xl <Card className="bg-card border shadow-sm"> <CardContent className="p-6">{/* ... */}</CardContent> </Card> // Buttons — one primary per view; no gradient / 3D <Button>Save invoice</Button> <Button variant="outline">Cancel</Button> <Button variant="destructive">Delete</Button> <Button variant="ghost" size="icon" aria-label="Settings"><Settings className="size-4" /></Button> // Form (React 19 Actions) — label above input, inline errors <form action={formAction} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="name">Name</Label> <Input id="name" name="name" /> {state?.errors?.name && <p className="text-sm text-destructive">{state.errors.name}</p>} </div> <SubmitButton /> {/* useFormStatus() → disabled + pending label */} </form> // Table — muted header, hover rows, right-aligned actions; no zebra stripes, no centered content <TableRow className="hover:bg-muted/50"> <TableCell>John Smith</TableCell> <TableCell><Badge>Active</Badge></TableCell> <TableCell className="text-right"><Button variant="ghost" size="sm">Edit</Button></TableCell> </TableRow>
Motion
Motion gives feedback; it is not decoration. Fast (150-200ms), subtle, ease-out.
tsx<div className="transition-colors hover:bg-muted" /> <Card className="transition-shadow hover:shadow-md" /> // Framer Motion enter: { opacity: 0, y: 8 } → { opacity: 1, y: 0 }, duration 0.2, easeOut // List stagger: 0.05 max · press: whileTap={{ scale: 0.98 }}
Hover ~150ms · general ~200ms · cap 200ms · no bounce/elastic, no dramatic entrances, no animating everything. Respect prefers-reduced-motion.
NON-NEGOTIABLE USABILITY FLOOR
The creative layer never overrides these:
| Rule | Detail |
|---|---|
| Logo top-left, clickable to home | Always for apps/sites (NNGroup ~6x evidence) |
| No hamburger on desktop | Visible nav links at md+ |
| Icons labeled, one library | aria-label on icon-only buttons; never emoji as icons |
| Focus states | focus-visible:ring-2 ring-ring on every interactive element |
| Interactions <= 200ms | ease-out; no bounce |
| WCAG AA contrast | Light and dark |
| Empty states | Real sentence + next action — never bare "No data" |
| Loading states | Skeletons matching the real layout — not a centered spinner |
| Error states | Inline: what happened + what to do next |
| Hover / active feedback | Every button/row (hover:bg-muted, active:scale-[0.98]) |
| Disabled / pending | Submit buttons show working state (useActionState / useFormStatus) |
Review Checklist
- Root DESIGN.md exists and was re-read for this generation
- Every color/typeface/radius/motion value traces to a DESIGN.md token
- Signature element present on the flagship view; everything else quiet
- Zero AVOID-LIST patterns (scan
AVOID-LIST.mdgroups) - Nav matches the matrix · logo top-left + clickable · URL reflects state
- One icon library, labeled · type pairing not Inter-as-display
- Usability floor: focus/empty/loading/error/hover/pending all present, WCAG AA
- Final: would a user tell AI made it? → must be no. Would two different briefs get this same design? → must be no.
Design Craft — one identity per project, chosen on purpose, on top of a floor that never bends.

