Great interfaces are a collection of small, intentional details that compound. Apply this doctrine when building or reviewing any UI.
DESIGN.md wins. If this repo has a DESIGN.md, its fonts, tokens, budgets, and recipes override everything below — this skill covers the craft layer underneath any design language. Express every change in the styling system the project already uses (Tailwind, CSS Modules, styled-components, plain CSS); never introduce a second one for a fix.
Direction
Before coding a new surface, commit to a clear aesthetic point of view — the repo's DESIGN.md if one exists, otherwise a deliberate choice for the context. Intentionality matters more than intensity: refined minimalism and bold maximalism both work; timid, evenly-distributed defaults do not. Never ship generic AI aesthetics: stock font stacks chosen by habit, purple-gradient-on-white, cookie-cutter card grids with no context-specific character. Match implementation effort to the vision — maximalist designs need elaborate effects, minimal ones need precision in spacing and type.
Typography
Good typography is mostly restraint: a sensible scale, comfortable spacing, enough contrast.
- Few fonts, few weights. Rarely more than two or three families. Pair for contrast (display vs body), not similarity. Hierarchy comes from size and weight used sparingly.
- Scale, not one-offs. Sizes come from the project's type scale. Heading sizes descend with level — pick the tag from the document outline, control size with CSS, never skip levels.
- Line-height by role. Headings ~
1.1, body1.5–1.6, always unitless. - Letter-spacing by size. Slightly negative on large headings, slightly positive on small uppercase labels, neither on body.
- Cap the measure. Long-form text at 60–75 characters per line (
max-w-2xlat 16px body, or65ch). - Wrap deliberately.
text-wrap: balanceon headings,text-wrap: prettyon descriptions; neither in long-form text. - Tabular numbers on anything that changes.
tabular-numson timers, counters, prices, table columns. - Natural case in copy. Store text in sentence case; uppercase via
text-transformonly where the design language sanctions it. - Mechanics.
.woff2for web fonts. CSS properties over raw tags (font-weight: 650,font-variant-numeric: tabular-nums— notfont-variation-settings/font-feature-settings).font-synthesis: noneso missing weights fail visibly.antialiasedonce on the root layout. Inputs ≥16pxon mobile (text-base sm:text-sm) or iOS zooms. Body ≥16px; contrast ≥4.5:1(3:1for large text).
Surfaces
- Concentric radius. When nesting rounded elements:
outer = inner + padding. Matching radii on parent and child is the most common thing that makes UI feel subtly off. Derive radii from the project's radius token where one exists. - Shadows over borders. Layered transparent
box-shadow(1px ring + tight lift + soft ambient) adapts to any background and animates smoothly; flat borders do neither. Use the project's shadow utility if it has one; otherwise:
shadow-[0px_0px_0px_1px_rgba(0,0,0,0.06),0px_1px_2px_-1px_rgba(0,0,0,0.06),0px_2px_4px_0px_rgba(0,0,0,0.04)] dark:shadow-[0px_0px_0px_1px_rgba(255,255,255,0.06),0px_1px_2px_-1px_rgba(255,255,255,0.03),0px_2px_4px_0px_rgba(0,0,0,0.2)]
Strengthen the same layers for hover/selected. Extract a shared component after the third repetition, with transition-shadow duration-200.
- Image outlines. Inset
1pxoutline on images/media for separation: pure black at 10% in light mode, pure white at 10% in dark — never a tinted neutral, which reads as dirt on the edge.[outline:1px_solid_rgba(0,0,0,0.1)] [outline-offset:-1px]. - Optical alignment. When geometric centering looks off (icon+text buttons, play triangles), nudge until it looks centered; the best fix is the SVG viewBox itself.
- Hit areas. ≥ 44×44px for touch, ≥ 40×40px on desktop. Extend with a pseudo-element if the visible control is smaller; never overlap two hit areas.
Motion
- Interruptible by default. CSS transitions and springs retarget mid-flight; keyframes don't. Transitions/springs for interactive states (hover, toggle, dismiss), keyframes only for one-shot sequences (page load). With a motion library, prefer
type: "spring", bounce: 0. - Split and stagger enters. Don't animate one container — break content into semantic chunks with ~60–100ms stagger, combining
opacity, smalltranslateY, andblur:
@keyframes enter {
from { opacity: 0; transform: translateY(8px); filter: blur(4px); }
}
.animate-enter {
animation: enter 600ms cubic-bezier(0.2, 0, 0, 1) both;
animation-delay: calc(80ms * var(--stagger, 0));
}
- Exits subtler than enters. Small fixed
translateY(~-12px) + blur + fade — never a full reversal of the enter path, never zero motion. - Icon swaps. Cross-fade in place with opacity/scale/blur (scale
0.25→1, blur4px→0);AnimatePresence mode="popLayout"with a spring, or an absolute-positioned CSS cross-fade without a library. - Press feedback.
active:scale-[0.96]on buttons — never below0.95. - No animation on first paint for default-state elements:
initial={false}onAnimatePresence. - Name transition properties. Never
transition: all— list exact properties.will-changeonly fortransform/opacity/filter, only after observing first-frame stutter. - Respect
prefers-reduced-motionon every entrance and press effect.
Review checklist
- Fonts, tokens, and budgets match
DESIGN.md(if present) - Sizes from the type scale; headings descend with level; line-height unitless by role
-
balanceon headings,prettyon descriptions, measure capped on long-form -
tabular-numson changing numbers;antialiasedon root; inputs ≥ 16px on mobile - Concentric radii on nested surfaces
- Layered shadows (or the project's shadow utility) instead of flat borders
- Images have the 1px untinted outline
- Icon+text pairs optically aligned; hit areas ≥ 40/44px
- Enters split and staggered; exits subtler; interactive motion interruptible
-
active:scale-[0.96]on primary press targets; reduced-motion honored - No
transition: all;will-changerare and specific