Web UI Best Practices
When invoked generally, apply these opinionated constraints for building better interfaces and for any UI work in this conversation.
When invoked with a file specified (i.e. web-ui-best-practices <file>), review the file against all constraints below and output:
- violations (quote the exact line/snippet)
- why it matters (1 short sentence)
- a concrete fix (code-level suggestion)
Stack
- MUST use Tailwind CSS defaults unless custom values already exist or are explicitly requested
- MUST use
motion/react(formerlyframer-motion) when JavaScript animation is required - SHOULD use
tw-animate-cssfor entrance and micro-animations in Tailwind CSS - MUST use
cnutility (clsx+tailwind-merge) for class logic
Components
- MUST use accessible component primitives for anything with keyboard or focus behavior (
Base UI,Radix,React-Aria) - MUST use the project’s existing component primitives first
- NEVER mix primitive systems within the same interaction surface
- SHOULD prefer
Base UIfor new primitives if compatible with the stack - MUST add an
aria-labelto icon-only buttons - NEVER rebuild keyboard or focus behavior by hand unless explicitly requested
Interaction
- MUST use an
AlertDialogfor destructive or irreversible actions - SHOULD use structural skeletons for loading states
- NEVER use
h-screen, useh-dvh - MUST show errors next to where the action happens
- NEVER block paste in
inputortextareaelements
Navigation & State
- SHOULD reflect UI state in the URL (filters, tabs, pagination, expanded panels via query params)
- MUST use
<a>/<Link>for navigation (Cmd/Ctrl+click and middle-click should work) - SHOULD deep-link stateful UI (if it uses
useState, consider URL sync vianuqsor similar) - MUST require confirmation or undo window for destructive actions (never immediate)
Touch & Interaction
- SHOULD set
touch-action: manipulationon tappable controls (prevents double-tap zoom delay) - SHOULD set
-webkit-tap-highlight-colorintentionally - MUST use
overscroll-behavior: containin modals/drawers/sheets - SHOULD during drag: disable text selection,
inerton dragged elements - SHOULD use
autoFocussparingly (desktop only, single primary input; avoid on mobile)
Animation
- NEVER add animation unless it is explicitly requested
- MUST animate only compositor props (
transform,opacity) - NEVER animate layout properties (
width,height,top,left,margin,padding) - SHOULD avoid animating paint properties (
background,color) except for small, local UI (text, icons) - SHOULD use
ease-outon entrance - NEVER exceed
200msfor interaction feedback - MUST pause looping animations when off-screen
- SHOULD respect
prefers-reduced-motion - NEVER introduce custom easing curves unless explicitly requested
- SHOULD avoid animating large images or full-screen surfaces
- SHOULD never
transition: all—list properties explicitly - SHOULD set correct
transform-origin - SHOULD have animations be interruptible—respond to user input mid-animation
Typography
- MUST use
text-balancefor headings andtext-prettyfor body/paragraphs - MUST use
tabular-numsfor data - SHOULD use
truncateorline-clampfor dense UI - NEVER modify
letter-spacing(tracking-*) unless explicitly requested - SHOULD use
…not... - SHOULD use curly quotes
""not straight" - SHOULD have non-breaking spaces:
10 MB,⌘ K, brand names - SHOULD have loading states end with
…:"Loading…","Saving…" - SHOULD use
font-variant-numeric: tabular-numsfor number columns/comparisons - SHOULD have flex children need
min-w-0to allow text truncation - SHOULD handle empty states—don't render broken UI for empty strings/arrays
- SHOULD consider that user-generated input fields may have short, average, and very long inputs
Layout
- MUST use a fixed
z-indexscale (no arbitraryz-*) - SHOULD use
size-*for square elements instead ofw-*+h-*
Safe Areas & Layout
- MUST respect
env(safe-area-inset-*)for full-bleed layouts and fixed elements on notched devices - SHOULD avoid unwanted scrollbars (use
overflow-x-hiddenon containers and fix overflow at the source) - SHOULD prefer flex/grid over JS measurement for layout
Dark Mode & Theming
- MUST set
color-scheme: darkon<html>for dark themes (fixes scrollbars and native inputs) - SHOULD set
<meta name="theme-color">to match the page background - SHOULD set explicit
background-colorandcoloron native<select>(Windows dark mode)
Locale & i18n
- MUST use
Intl.DateTimeFormat(no hardcoded date/time formats) - MUST use
Intl.NumberFormat(no hardcoded number/currency formats) - SHOULD detect language via
Accept-Language/navigator.languages, not IP
Hydration Safety
- MUST provide
onChangefor inputs withvalue(or usedefaultValuefor uncontrolled inputs) - MUST guard date/time rendering against hydration mismatches (server vs client)
- SHOULD use
suppressHydrationWarningonly where truly needed
Hover & Interactive States
- SHOULD provide
hover:states for buttons/links (visual feedback) - SHOULD increase contrast for interactive states (hover/active/focus more prominent than rest)
Images
- MUST set explicit
widthandheighton<img>(prevents CLS) - SHOULD set
loading="lazy"for below-fold images - SHOULD set
priorityorfetchpriority="high"for above-fold critical images
Performance
- NEVER animate large
blur()orbackdrop-filtersurfaces - NEVER apply
will-changeoutside an active animation - NEVER use
useEffectfor anything that can be expressed as render logic - SHOULD virtualize large lists (>50 items) (e.g.
virtuaorcontent-visibility: autowhere appropriate) - MUST avoid layout reads during render (
getBoundingClientRect,offsetHeight,offsetWidth,scrollTop) - SHOULD batch DOM reads/writes (avoid interleaving)
- SHOULD prefer uncontrolled inputs; controlled inputs must be cheap per keystroke
- SHOULD add
<link rel="preconnect">for CDN/asset domains - SHOULD preload critical fonts (
<link rel="preload" as="font">) and usefont-display: swap
Design
- NEVER use gradients unless explicitly requested
- NEVER use purple or multicolor gradients
- NEVER use glow effects as primary affordances
- SHOULD use Tailwind CSS default shadow scale unless explicitly requested
- MUST give empty states one clear next action
- SHOULD limit accent color usage to one per view
- SHOULD use existing theme or Tailwind CSS color tokens before introducing new ones
Design Direction (when aesthetics requested)
- MUST choose a clear aesthetic direction and execute consistently (e.g., brutally minimal, editorial, utilitarian, playful)
- SHOULD use deliberate typography (1 display + 1 body) and avoid accidental defaults; follow product font system when it exists
- SHOULD define theme/palette via CSS variables; use one dominant base + sharp accent
- SHOULD match implementation complexity to the aesthetic (minimal = restraint; maximal = elaborate but controlled)
- If motion is explicitly requested: prefer one cohesive high-impact sequence (load stagger, scroll reveal) over scattered micro-interactions
- SHOULD avoid cookie-cutter layout/component patterns; make 1-2 distinctive choices grounded in context
Content & Copy
- SHOULD use active voice ("Install the CLI" not "The CLI will be installed")
- SHOULD use specific button labels ("Save API Key" not "Continue")
- SHOULD write error messages with a fix/next step, not just the problem
- SHOULD write in second person; avoid first person
- SHOULD use
&over "and" where space-constrained
Anti-patterns (flag these)
user-scalable=noormaximum-scale=1disabling zoomonPastewithpreventDefaulttransition: alloutline-nonewithout afocus-visiblereplacement- Inline
onClicknavigation without<a>/<Link> <div>/<span>with click handlers (use<button>)- Images without dimensions
- Large arrays
.map()without virtualization - Form inputs without labels
- Icon buttons without
aria-label - Hardcoded date/number formats (use
Intl.*) autoFocuswithout clear justification
Rules if not found in primitives
The below rules apply if the pattern is either explicitly requested or not found in the primitive itself already.
Accessibility
- Icon-only buttons need
aria-label - Form controls need
<label>oraria-label - Interactive elements need keyboard handlers (
onKeyDown/onKeyUp) <button>for actions,<a>/<Link>for navigation (not<div onClick>)- Images need
alt(oralt=""if decorative) - Decorative icons need
aria-hidden="true" - Async updates (toasts, validation) need
aria-live="polite" - Use semantic HTML (
<button>,<a>,<label>,<table>) before ARIA - Headings hierarchical
<h1>–<h6>; include skip link for main content scroll-margin-topon heading anchors
Focus States
- Interactive elements need visible focus:
focus-visible:ring-*or equivalent - Never
outline-none/outline: nonewithout focus replacement - Use
:focus-visibleover:focus(avoid focus ring on click) - Group focus with
:focus-withinfor compound controls
Forms
- Inputs need
autocompleteand meaningfulname - Use correct
type(email,tel,url,number) andinputmode - Never block paste (
onPaste+preventDefault) - Labels clickable (
htmlForor wrapping control) - Disable spellcheck on emails, codes, usernames (
spellCheck={false}) - Checkboxes/radios: label + control share single hit target (no dead zones)
- Submit button stays enabled until request starts; spinner during request
- Errors inline next to fields; focus first error on submit
- Placeholders end with
…and show example pattern autocomplete="off"on non-auth fields to avoid password manager triggers- Warn before navigation with unsaved changes (
beforeunloador router guard)