UI Design Principles
Core principles for building high-quality UI. Reference these when creating components, layouts, and pages.
Spacing System
Use a consistent spacing scale. Never use arbitrary values.
- 4px — tight inner padding (badges, pills)
- 8px — default inner padding, small gaps
- 12px — compact component padding
- 16px — standard component padding, list gaps
- 24px — card padding, medium section gaps
- 32px — large component gaps
- 48px — section padding (mobile)
- 64px — section padding (tablet)
- 80px — section padding (desktop small)
- 120px — section padding (desktop large)
Typography Hierarchy
Every page should have a clear type hierarchy. Never use more than 3-4 font sizes per page.
- Display: 48-72px — hero headlines only
- H1: 36-48px — page titles
- H2: 24-32px — section titles
- H3: 20-24px — card titles, subsections
- Body: 16-18px — paragraph text
- Small: 14px — captions, labels, metadata
- Micro: 12px — badges, overlines
Line height: 1.1-1.2 for headlines, 1.5-1.6 for body text.
Type Rendering (Font Smoothing)
Browsers default to subpixel antialiasing, which renders text heavier and slightly blurrier than design tools show it (Figma uses its own renderer, so what you approved is not what ships). Fix it once, globally:
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
- macOS only: Windows ClearType ignores both properties. You're fixing the platform where the Figma-to-browser delta is most visible.
- No layout impact: glyph widths, line breaks, and spacing stay identical. Purely how edges meet the pixel grid.
- Most visible on dark backgrounds, where subpixel rendering is at its heaviest.
- The catch: grayscale rendering removes visual weight from letterforms. Don't go below
font-weight: 400 at small sizes, and bump weight up as size goes down — what reads fine at 400 on 16px needs a bump at 12px to hold the same presence.
- Font format is irrelevant here: WOFF2/OTF/TTF rasterize identically. Use WOFF2 for file size, not rendering.
Layout Rules
- Max content width: 1200-1280px
- Text max-width: 640-720px for readability
- Use 12-column grid for desktop, collapse to 4 for mobile
- Cards: consistent padding, consistent radius, consistent shadow
- Sections: alternate between full-width and contained widths for rhythm
- Always group related elements with less space than unrelated elements (proximity principle)
Visual Hierarchy
- One primary action per section (single CTA)
- Use size, weight, and color to establish 3 levels: primary, secondary, tertiary
- Dark backgrounds for emphasis sections, light for breathing room
- Icons support text, never replace it (except established patterns like search, close, menu)
Color Systems
- Prefer OKLCH/OKLab for palette and gradient work when the stack supports it.
- Build color ramps around real roles: background, surface, border, text, muted text, accent, danger, success.
- Check contrast on every text/background pair before shipping.
- Keep accent colors scarce; one strong accent usually beats several competing accents.
- Use
gradients for gradient-heavy work and swiss-design or another design-system skill when the user requests a specific visual language.
Component Patterns
- Buttons: minimum 44px height for touch, 12-24px horizontal padding, never less than 80px wide
- Cards: consistent internal structure (image/icon → title → description → action)
- Inputs: minimum 44px height, visible labels (not just placeholder), clear focus states
- Navigation: logo left, links center or right, CTA far right
Responsive
- Design desktop-first for marketing sites, mobile-first for apps
- Breakpoints: 640px (mobile), 768px (tablet), 1024px (desktop), 1280px (wide)
- Stack horizontal layouts vertically on mobile
- Reduce section padding by ~40% on mobile
- Headlines scale down 1-2 steps on mobile
Common Mistakes to Avoid
- Inconsistent spacing (mixing 15px and 16px)
- Too many font sizes on one page
- Buttons that look like links and links that look like buttons
- Sections with no breathing room between them
- Text over images without sufficient contrast/overlay
- Centering everything — left-align body text
AI Slop Detection
Generated UI has recognizable tells. If you spot these patterns, you're probably producing generic output instead of intentional design. Fix or remove them:
- Gratuitous gradients — purple-to-blue or teal-to-cyan backgrounds with no design rationale
- Glassmorphism everywhere — frosted glass cards with backdrop-blur used as a default style, not a deliberate choice
- Generic hero layout — centered text over a stock gradient with a "Get Started" button and no real content hierarchy
- Decoration over function — floating blobs, animated mesh backgrounds, glow effects that don't support the content
- Same component repeated 3x — three feature cards, three pricing tiers, three testimonials, all identical structure with placeholder-quality copy
- Over-rounded everything —
rounded-2xl or rounded-3xl on every surface with no variation
- Default shadow stack —
shadow-lg on cards, shadow-xl on modals, no thought about elevation system
The fix is always the same: ask what the design is trying to communicate, then choose the simplest visual treatment that achieves it.
Fluid Scale (Tailwind Config)
Map fluid clamp() values into Tailwind so you use utilities, not raw CSS:
// tailwind.config.ts
export default {
theme: {
extend: {
fontSize: {
'fluid-sm': 'clamp(0.875rem, 0.5vw + 0.75rem, 1rem)',
'fluid-base': 'clamp(1rem, 1vw + 0.75rem, 1.25rem)',
'fluid-lg': 'clamp(1.25rem, 2vw + 0.75rem, 2rem)',
'fluid-xl': 'clamp(1.5rem, 3vw + 0.75rem, 3rem)',
'fluid-2xl': 'clamp(2rem, 5vw + 1rem, 4rem)',
'fluid-hero': 'clamp(2.5rem, 6vw + 1rem, 5rem)',
},
spacing: {
'fluid-xs': 'clamp(0.25rem, 0.5vw, 0.5rem)',
'fluid-s': 'clamp(0.5rem, 1vw, 0.75rem)',
'fluid-m': 'clamp(1rem, 2vw + 0.5rem, 2rem)',
'fluid-l': 'clamp(1.5rem, 4vw + 0.5rem, 4rem)',
'fluid-xl': 'clamp(2rem, 6vw + 1rem, 8rem)',
'fluid-2xl': 'clamp(3rem, 10vw + 1rem, 12rem)',
},
},
},
}
Always combine rem + vw in the preferred value. Pure vw doesn't scale when users zoom to 200%.
Viewport Units
| Unit |
Browser UI state |
Use for |
svh |
Fully expanded (smallest) |
Default. hero sections, modals, anything that must fit on load |
lvh |
Fully retracted (largest) |
Backgrounds, decorative elements |
dvh |
Dynamic, updates on scroll |
Sparingly. chat interfaces, overlays tracking exact visible space |
Always provide a fallback: height: 100vh; height: 100svh;. Avoid dvh for primary layout.
Touch, Hover, Motion
Touch targets (in globals.css inside @layer base):
@media (pointer: coarse) {
button, a, [role="button"] { min-height: 44px; min-width: 44px; }
}
Hover gating for transforms:
@media (hover: hover) and (pointer: fine) {
.hover-lift:hover { transform: translateY(-4px); }
}
Reduced motion with Tailwind:
<div className="motion-safe:animate-fade-in motion-reduce:animate-none">
Pretext (Text Measurement)
Use Pretext (bun add @chenglou/pretext) when layout depends on text height: virtualized lists, masonry, shrink-wrapping chat bubbles, text flowing around obstacles.
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('Your text here', '16px Inter') // one-time, expensive
const { height, lineCount } = layout(prepared, width, 24) // cheap, call on every resize
Use a named font, not system-ui. The font string must match your CSS exactly.
1---2name: ui-principles3description: Core principles for building high-quality UI: spacing scale, typography hierarchy, layout rules, max content widths, grid systems, card consistency, proximity grouping, and section rhythm. Reference these when creating components, laying out pages, choosing font sizes, picking padding values, or setting up a new design from scratch. Triggers: ui principles, spacing scale, typography, type hierarchy, layout rules, design fundamentals, what padding, what font size, design system basics.4---56# UI Design Principles78Core principles for building high-quality UI. Reference these when creating components, layouts, and pages.910## Spacing System1112Use a consistent spacing scale. Never use arbitrary values.13- 4px — tight inner padding (badges, pills)14- 8px — default inner padding, small gaps15- 12px — compact component padding16- 16px — standard component padding, list gaps17- 24px — card padding, medium section gaps18- 32px — large component gaps19- 48px — section padding (mobile)20- 64px — section padding (tablet)21- 80px — section padding (desktop small)22- 120px — section padding (desktop large)2324## Typography Hierarchy2526Every page should have a clear type hierarchy. Never use more than 3-4 font sizes per page.27- Display: 48-72px — hero headlines only28- H1: 36-48px — page titles29- H2: 24-32px — section titles30- H3: 20-24px — card titles, subsections31- Body: 16-18px — paragraph text32- Small: 14px — captions, labels, metadata33- Micro: 12px — badges, overlines3435Line height: 1.1-1.2 for headlines, 1.5-1.6 for body text.3637## Type Rendering (Font Smoothing)3839Browsers default to subpixel antialiasing, which renders text heavier and slightly blurrier than design tools show it (Figma uses its own renderer, so what you approved is not what ships). Fix it once, globally:4041```css42* {43 -webkit-font-smoothing: antialiased;44 -moz-osx-font-smoothing: grayscale;45}46```4748- macOS only: Windows ClearType ignores both properties. You're fixing the platform where the Figma-to-browser delta is most visible.49- No layout impact: glyph widths, line breaks, and spacing stay identical. Purely how edges meet the pixel grid.50- Most visible on dark backgrounds, where subpixel rendering is at its heaviest.51- The catch: grayscale rendering removes visual weight from letterforms. Don't go below `font-weight: 400` at small sizes, and bump weight up as size goes down — what reads fine at 400 on 16px needs a bump at 12px to hold the same presence.52- Font format is irrelevant here: WOFF2/OTF/TTF rasterize identically. Use WOFF2 for file size, not rendering.5354## Layout Rules5556- Max content width: 1200-1280px57- Text max-width: 640-720px for readability58- Use 12-column grid for desktop, collapse to 4 for mobile59- Cards: consistent padding, consistent radius, consistent shadow60- Sections: alternate between full-width and contained widths for rhythm61- Always group related elements with less space than unrelated elements (proximity principle)6263## Visual Hierarchy6465- One primary action per section (single CTA)66- Use size, weight, and color to establish 3 levels: primary, secondary, tertiary67- Dark backgrounds for emphasis sections, light for breathing room68- Icons support text, never replace it (except established patterns like search, close, menu)6970## Color Systems7172- Prefer OKLCH/OKLab for palette and gradient work when the stack supports it.73- Build color ramps around real roles: background, surface, border, text, muted text, accent, danger, success.74- Check contrast on every text/background pair before shipping.75- Keep accent colors scarce; one strong accent usually beats several competing accents.76- Use `gradients` for gradient-heavy work and `swiss-design` or another design-system skill when the user requests a specific visual language.7778## Component Patterns7980- Buttons: minimum 44px height for touch, 12-24px horizontal padding, never less than 80px wide81- Cards: consistent internal structure (image/icon → title → description → action)82- Inputs: minimum 44px height, visible labels (not just placeholder), clear focus states83- Navigation: logo left, links center or right, CTA far right8485## Responsive8687- Design desktop-first for marketing sites, mobile-first for apps88- Breakpoints: 640px (mobile), 768px (tablet), 1024px (desktop), 1280px (wide)89- Stack horizontal layouts vertically on mobile90- Reduce section padding by ~40% on mobile91- Headlines scale down 1-2 steps on mobile9293## Common Mistakes to Avoid9495- Inconsistent spacing (mixing 15px and 16px)96- Too many font sizes on one page97- Buttons that look like links and links that look like buttons98- Sections with no breathing room between them99- Text over images without sufficient contrast/overlay100- Centering everything — left-align body text101102## AI Slop Detection103104Generated UI has recognizable tells. If you spot these patterns, you're probably producing generic output instead of intentional design. Fix or remove them:105106- **Gratuitous gradients** — purple-to-blue or teal-to-cyan backgrounds with no design rationale107- **Glassmorphism everywhere** — frosted glass cards with backdrop-blur used as a default style, not a deliberate choice108- **Generic hero layout** — centered text over a stock gradient with a "Get Started" button and no real content hierarchy109- **Decoration over function** — floating blobs, animated mesh backgrounds, glow effects that don't support the content110- **Same component repeated 3x** — three feature cards, three pricing tiers, three testimonials, all identical structure with placeholder-quality copy111- **Over-rounded everything** — `rounded-2xl` or `rounded-3xl` on every surface with no variation112- **Default shadow stack** — `shadow-lg` on cards, `shadow-xl` on modals, no thought about elevation system113114The fix is always the same: ask what the design is trying to communicate, then choose the simplest visual treatment that achieves it.115116## Fluid Scale (Tailwind Config)117118Map fluid `clamp()` values into Tailwind so you use utilities, not raw CSS:119120```js121// tailwind.config.ts122export default {123 theme: {124 extend: {125 fontSize: {126 'fluid-sm': 'clamp(0.875rem, 0.5vw + 0.75rem, 1rem)',127 'fluid-base': 'clamp(1rem, 1vw + 0.75rem, 1.25rem)',128 'fluid-lg': 'clamp(1.25rem, 2vw + 0.75rem, 2rem)',129 'fluid-xl': 'clamp(1.5rem, 3vw + 0.75rem, 3rem)',130 'fluid-2xl': 'clamp(2rem, 5vw + 1rem, 4rem)',131 'fluid-hero': 'clamp(2.5rem, 6vw + 1rem, 5rem)',132 },133 spacing: {134 'fluid-xs': 'clamp(0.25rem, 0.5vw, 0.5rem)',135 'fluid-s': 'clamp(0.5rem, 1vw, 0.75rem)',136 'fluid-m': 'clamp(1rem, 2vw + 0.5rem, 2rem)',137 'fluid-l': 'clamp(1.5rem, 4vw + 0.5rem, 4rem)',138 'fluid-xl': 'clamp(2rem, 6vw + 1rem, 8rem)',139 'fluid-2xl': 'clamp(3rem, 10vw + 1rem, 12rem)',140 },141 },142 },143}144```145146Always combine `rem + vw` in the preferred value. Pure `vw` doesn't scale when users zoom to 200%.147148## Viewport Units149150| Unit | Browser UI state | Use for |151|------|------------------|---------|152| `svh` | Fully expanded (smallest) | Default. hero sections, modals, anything that must fit on load |153| `lvh` | Fully retracted (largest) | Backgrounds, decorative elements |154| `dvh` | Dynamic, updates on scroll | Sparingly. chat interfaces, overlays tracking exact visible space |155156Always provide a fallback: `height: 100vh; height: 100svh;`. Avoid `dvh` for primary layout.157158## Touch, Hover, Motion159160Touch targets (in `globals.css` inside `@layer base`):161```css162@media (pointer: coarse) {163 button, a, [role="button"] { min-height: 44px; min-width: 44px; }164}165```166167Hover gating for transforms:168```css169@media (hover: hover) and (pointer: fine) {170 .hover-lift:hover { transform: translateY(-4px); }171}172```173174Reduced motion with Tailwind:175```jsx176<div className="motion-safe:animate-fade-in motion-reduce:animate-none">177```178179## Pretext (Text Measurement)180181Use Pretext (`bun add @chenglou/pretext`) when layout depends on text height: virtualized lists, masonry, shrink-wrapping chat bubbles, text flowing around obstacles.182183```js184import { prepare, layout } from '@chenglou/pretext'185const prepared = prepare('Your text here', '16px Inter') // one-time, expensive186const { height, lineCount } = layout(prepared, width, 24) // cheap, call on every resize187```188189Use a named font, not `system-ui`. The font string must match your CSS exactly.