Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Designing Frontend Interfaces
Overview
Distinctive interfaces come from committing to one aesthetic direction and executing it with a locked system of values. Generic interfaces come from picking each value ad hoc at the moment it is needed.
Core principle: Decide the system before you write the components. Every number in the finished CSS should trace back to a scale you chose deliberately.
This skill covers visual craft. Two companion skills cover the rest of interface quality:
- REQUIRED SUB-SKILL for anything with flows, forms, or non-trivial state: use
designing-user-experience - REQUIRED SUB-SKILL before shipping: use
building-accessible-interfaces - To critique a finished UI, use
reviewing-interface-quality
The Iron Law
NO COMPONENT CODE BEFORE THE DESIGN BRIEF AND TOKEN BLOCK EXISTThe brief is 8 lines. The token block is one CSS block. Together they cost about ninety seconds and they are what separates a designed interface from a decorated one.
Wrote components first? The values are already arbitrary. Extract them into a real scale before continuing — do not rationalize the numbers you happened to type.
Checklist
Create a task for each item and complete them in order:
- Write the design brief — 8 lines, in your response, before any file is created
- Choose an aesthetic direction — from
references/aesthetic-directions.md, or invent one, and name it - Lock the token block — type scale, color ramp, space scale, radius, shadow, motion. One CSS block.
- Build the layout skeleton — structure and spacing, no decoration yet
- Build components — every interactive element gets all five states
- Add the one memorable moment — the single thing someone will remember
- Self-review against the rubric — see Self-Review below; fix what fails
- Verify visually — render it and actually look at it
Step 1: The Design Brief
Write this out before creating any file. It is 8 lines of plain text in your response — not a document, not a file.
Purpose: What job does this interface do?
Audience: Who uses it, in what context, on what device?
Tone: One named direction (see references/aesthetic-directions.md)
Reference: A real-world visual analogue — a magazine, a era, a discipline
Palette: Base / surface / ink / one accent — as hue intentions, not hex yet
Type: Display face + text face, and why this pairing
Memorable: The one thing someone will describe to a friend
Restraint: What this design deliberately does NOT doThe Restraint line matters most. A design with no stated restraint becomes a design with every effect applied at once.
Worked example:
Purpose: Changelog for a developer infrastructure tool
Audience: Engineers skimming on a wide monitor, 30 seconds per visit
Tone: Technical Broadsheet — newspaper density meets terminal precision
Reference: Financial Times print edition, 1990s man pages
Palette: Warm newsprint base, near-black ink, one signal red for breaking changes
Type: Instrument Serif display / IBM Plex Mono text — editorial authority, machine detail
Memorable: Version numbers set enormous in the left margin, hanging outside the text column
Restraint: No cards. No shadows. No rounded corners. Rules and space only.Step 2: Lock the Token Block
Every value the interface uses comes from here. If a number is not in this block, it does not belong in a component.
:root {
/* TYPE — one ratio, applied consistently. 1.200 minor third (dense UI),
1.250 major third (general), 1.333 perfect fourth (marketing),
1.500 perfect fifth (editorial/poster). Pick ONE. */
--font-display: "Instrument Serif", Georgia, serif;
--font-text: "IBM Plex Mono", ui-monospace, monospace;
--step--1: 0.833rem; --step-0: 1rem; --step-1: 1.25rem;
--step-2: 1.563rem; --step-3: 1.953rem; --step-4: 2.441rem;
--step-5: 3.052rem; --step-6: 3.815rem;
/* SPACE — one base unit and its multiples. Never a value in between. */
--space-3xs: 0.25rem; --space-2xs: 0.5rem; --space-xs: 0.75rem;
--space-s: 1rem; --space-m: 1.5rem; --space-l: 2rem;
--space-xl: 3rem; --space-2xl: 4.5rem; --space-3xl: 7rem;
/* COLOR — a ramp, not a set of one-offs. One accent, used sparingly. */
--base: #f4f1ea; /* page */
--surface: #ffffff; /* raised */
--ink: #12100e; /* primary text */
--ink-2: #55504a; /* secondary text */
--ink-3: #8b857c; /* tertiary / meta */
--line: #ddd7cc; /* borders, rules */
--accent: #c1362f; /* <10% of visible surface */
/* FORM */
--radius: 0; /* commit: 0, or 2-4px, or a full 999px pill. Not "some of each". */
--border: 1px solid var(--line);
--shadow: none; /* if using shadows, define 2 — near and far. Never 5. */
/* MOTION */
--dur-fast: 120ms; --dur: 220ms; --dur-slow: 420ms;
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}Rules for the block:
| Token | Rule |
|---|---|
| Type scale | One ratio for the whole page. Deriving sizes by eye produces muddy hierarchy. |
| Space scale | Every margin/padding/gap is a --space-* token. No 13px. No 0.85rem. |
| Color ramp | Three ink levels is enough. A fourth means the hierarchy is unclear. |
| Accent | Exactly one, unless the direction is explicitly maximalist. Under 10% of surface. |
| Radius | One value. Mixed radii is the single loudest amateur signal. |
| Shadow | Zero or two. Two shadows = one tight contact shadow + one wide ambient. |
| Motion | Three durations, two easings. That covers every transition an interface needs. |
Step 3: Hierarchy
Vary one dimension at a time. Amateur hierarchy changes size and weight and color and spacing simultaneously; everything shouts and nothing leads.
| Need | Change | Keep fixed |
|---|---|---|
| Section heading vs. body | Size (2+ steps) | Weight, color |
| Emphasis inside a paragraph | Weight | Size, color |
| Metadata / captions | Color (to --ink-3) | Size, weight |
| Grouping | Space (a full step) | Everything else |
Space groups more reliably than lines or boxes. Before adding a border or a card, try doubling the gap between groups and halving it within them. Most "needs a card" instincts are actually "needs correct spacing."
Step 4: Anti-Slop
Generic AI interface output has a specific, enumerable signature. These are not stylistic preferences — they are the exact defaults that make output identifiable on sight.
| Never | Instead |
|---|---|
Inter, Roboto, Arial, system-ui as the display face | A face with an opinion — see references/typography.md |
| Purple/violet → blue gradient on white | Commit to a real palette from a real reference |
rounded-lg / rounded-xl on everything | One radius, chosen; or zero radius |
A shadow-md on every surface | Shadows only where something genuinely floats |
| Hero → 3 feature cards → CTA | Let the content decide the structure |
A centered max-w-4xl column for the whole page | Vary measure by content type; break the grid at least once |
text-gray-500 body text on white | Real ink colors with intent; body text at full contrast |
| Emoji standing in for icons | A real icon set, or no icons |
| Six weights of one neutral sans | Two faces, three weights, maximum |
| Every section the same vertical rhythm | Vary section spacing to express importance |
The convergence trap: across separate generations, do not keep reaching for the same "safe distinctive" choices. Space Grotesk, a dark slate background, and a lime-green accent is now its own cliché. Vary light and dark, serif and sans and mono, dense and airy, across designs. Check references/aesthetic-directions.md and pick one you did not pick last time.
Rationalization table:
| Thought | Reality |
|---|---|
| "The user didn't specify a style, so neutral is safest" | Unspecified means you choose. Neutral is a choice, and it is the worst one. |
| "It's just a quick demo" | Demos are what people look at. The brief takes ninety seconds. |
| "Inter is genuinely a good typeface" | It is. It is also the single strongest generic-AI signal. Use it for body text at most, never for display. |
| "A card grid is the clearest way to show features" | It is the most common way. Try a table, a list with rules, a stepped layout, an editorial spread. |
| "I'll add the distinctive touches at the end" | Distinctiveness is structural. Bolted on at the end it reads as decoration. |
| "Tailwind defaults are a design system" | They are a palette of defaults. Configure them, or you ship the defaults everyone ships. |
Step 5: Component States
Every interactive element gets all five. Missing states are the most common gap in generated UI.
.btn { /* rest */ }
.btn:hover { /* pointer only — must not be the sole affordance */ }
.btn:active { /* pressed — a real transform, not just a color shift */ }
.btn:focus-visible{ outline: 2px solid var(--accent); outline-offset: 2px; }
.btn:disabled { /* visibly inert, and still readable */ }
.btn[aria-busy] { /* loading — reserve the space, don't collapse the layout */ }Never remove focus outlines. If the default outline is ugly, restyle it — do not set outline: none. See building-accessible-interfaces.
Step 6: Verify Visually
Rendering code and reading it back is not verification. Look at the actual pixels.
# Static HTML
python3 -c "
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(viewport={'width':1440,'height':900})
pg.goto('file:///abs/path/index.html'); pg.wait_for_timeout(600)
pg.screenshot(path='/tmp/desktop.png', full_page=True)
pg.set_viewport_size({'width':390,'height':844})
pg.screenshot(path='/tmp/mobile.png', full_page=True)
b.close()"Then read both PNGs and check them against the rubric. For dev servers, use the testing-webapps skill's with_server.py helper.
What to look for that only shows in the render: text overflowing containers, collapsed layouts at 390px, invisible low-contrast text, elements touching viewport edges, a heading that wraps to one orphaned word, images without dimensions causing layout shift.
Self-Review
Score each line honestly. Any no means fix it, not explain it.
- [ ] Every spacing value in the CSS is a token from the block
- [ ] The type scale is one ratio; no size was chosen by eye
- [ ] Exactly one accent color, under 10% of visible surface
- [ ] One radius value throughout
- [ ] A stranger could name the aesthetic direction from a screenshot
- [ ] The
Memorableline from the brief is actually present in the build - [ ] The
Restraintline was actually respected - [ ] Nothing from the Anti-Slop table appears
- [ ] All five states exist on every interactive element
- [ ] Focus is visible and never suppressed
- [ ] Verified at 1440px and 390px by looking at screenshots
- [ ] Body text passes 4.5:1; large text passes 3:1
Reference Files
Load these when you reach the relevant decision — not upfront.
| File | Load when |
|---|---|
references/aesthetic-directions.md | Step 1 — choosing and parameterizing a direction |
references/typography.md | Choosing faces, building the scale, setting text |
references/color-and-theme.md | Building the ramp, dark mode, contrast math |
references/layout-and-composition.md | Grids, breakpoints, breaking the grid, z-index |
references/motion.md | Any animation beyond a hover color change |
Common Mistakes
Tokens declared, then ignored. The :root block exists but components use padding: 14px. Grep your CSS for raw px values before finishing.
Distinctive typeface, default everything else. Swapping Inter for a display face while keeping the card grid, the uniform radius, and the gray body text produces generic output in a costume.
Maximalism as an excuse for incoherence. Maximalist directions still need one type scale, one space scale, one palette. More elements, same system.
The brief describes a design the build does not deliver. Re-read the brief at Step 7. If Restraint: no shadows and the build has shadows, the build is wrong.
Complexity mismatched to the vision. A refined-minimal direction implemented with elaborate scroll animations reads as confused. Match implementation effort to the stated tone.

