Web Interface Guidelines
Nicholas
@nicholas
--- created: 2026-05-23 description: "Guidelines for building great interfaces on the web. Covers interactions, animations, layout, content, forms, performance & design."
- Uploaded
- Uploaded May 27, 2026
- File type
- MD
- Queried
- 0
Full document
Showing the full document.
---
title: "Web Interface Guidelines"
source: "https://vercel.com/design/guidelines"
author:
published:
created: 2026-05-23
description: "Guidelines for building great interfaces on the web. Covers interactions, animations, layout, content, forms, performance & design."
tags:
- "clippings"
---
Interfaces succeed because of hundreds of choices. This is a living, non-exhaustive list of those decisions. Most guidelines are framework-agnostic, some specific to React/Next.js. [Feedback is welcome](https://github.com/vercel-labs/web-interface-guidelines/tree/main).
## Interactions
- **[Keyboard works everywhere.](#keyboard-works-everywhere)** All flows are keyboard-operable & follow the [WAI-ARIA Authoring Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/).
- **[Clear focus.](#clear-focus)** Every focusable element shows a visible focus ring. Prefer `:focus-visible` over `:focus` to avoid distracting pointer users. Set `:focus-within` for grouped controls.
- **[Manage focus.](#manage-focus)** Use focus traps, move & return focus according to the [WAI-ARIA Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/).
- **[Match visual & hit targets.](#match-visual-hit-targets)** Exception: if the visual target is < 24px, expand its hit target to ≥ 24px. On mobile, the minimum size is 44px.
- **[Mobile input size.](#mobile-input-size)** `<input>` font size is ≥ 16px on mobile to prevent iOS Safari auto-zoom/pan on focus. Or set `<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />`.
- **[Respect zoom.](#respect-zoom)** Never disable browser zoom.
- **[Hydration-safe inputs.](#hydration-safe-inputs)** Inputs must not lose focus or value after hydration.
- **[Don’t block paste.](#dont-block-paste)** Never disable paste in `<input>` or `<textarea>`.
- Show a loading indicator & keep the original label.
- If you show a spinner/skeleton, add a short show-delay (~150–300 ms) & a minimum visible time (~300–500 ms) to avoid flicker on fast responses. The `<Suspense>` component in React does this automatically.
- **[URL as state.](#url-as-state)** Persist state in the URL so share, refresh, Back/Forward navigation work e.g., [nuqs](https://nuqs.dev/).
- **[Optimistic updates.](#optimistic-updates)** Update the UI immediately when success is likely; reconcile on server response. On failure, show an error & roll back or provide Undo.
- Menu options that open a follow-up e.g., "Rename…" & loading/processing states e.g., "Loading…", "Saving…", "Generating…" end with an ellipsis.
- **[Confirm destructive actions.](#confirm-destructive-actions)** Require confirmation or provide Undo with a safe window.
- **[Prevent double-tap zoom on controls.](#prevent-double-tap-zoom-on-controls)** Set `touch-action: manipulation`.
- **[Tap highlight follows design.](#tap-highlight-follows-design)** Set `webkit-tap-highlight-color`.
- Controls minimize finickiness with generous hit targets, clear affordances, & predictable interactions, e.g., [prediction cones](https://x.com/JohnPhamous/status/1657083267299028992).
- **[Tooltip timing.](#tooltip-timing)** Delay the first tooltip in a group; [subsequent peers have no delay](https://x.com/emilkowalski_/status/1962500739336462340).
- **[Overscroll behavior.](#overscroll-behavior)** Set `overscroll-behavior: contain` intentionally e.g., in modals/drawers.
- **[Scroll positions persist.](#scroll-positions-persist)** Back/Forward restores prior scroll.
- **[Autofocus for speed.](#autofocus-for-speed)** On desktop screens with a single primary input, autofocus. Rarely autofocus on mobile because the keyboard opening can cause layout shift.
- **[No dead zones.](#no-dead-zones)** If part of a control looks interactive, it should be interactive. Don’t leave users guessing where to interact.
- **[Deep-link everything.](#deep-link-everything)** Filters, tabs, pagination, expanded panels, anytime `useState` is used.
- Disable text selection & apply [`inert`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert) (which prevents interaction) while an element is dragged so selection/hover don't occur simultaneously.
- **[Links are links.](#links-are-links)** Use `<a>` or `<Link>` for navigation so standard browser behaviors work (Cmd/Ctrl+Click, middle-click, right-click to open in a new tab). Never substitute with `<button>` or `<div>` for navigational links.
- **[Announce async updates.](#announce-async-updates)** Use polite aria-live for toasts & inline validation.
- **[Locale-aware keyboard shortcuts.](#locale-aware-keyboard-shortcuts)** Internationalize keyboard shortcuts for non-QWERTY layouts. Show platform-specific symbols.
## Animations
- **[Honor `prefers-reduced-motion`.](#honor-prefers-reduced-motion)** Provide a reduced-motion variant.
- **[Implementation preference.](#implementation-preference)** Prefer CSS, avoid main-thread JS-driven animations when possible.
- Preference: CSS > Web Animations API > JavaScript libraries e.g., [motion](https://www.npmjs.com/package/motion).
- **[Compositor-friendly.](#compositor-friendly)** Prioritize GPU-accelerated properties (`transform`, `opacity`) & avoid properties that trigger reflows/repaints (`width`, `height`, `top`, `left`).
- **[Necessity check.](#necessity-check)** Only animate when it clarifies cause & effect or when it adds deliberate delight e.g., [the northern lights](https://x.com/JohnPhamous/status/1831380516509278561).
- **[Easing fits the subject.](#easing-fits-the-subject)** Choose easing based on what changes (size, distance, trigger).
- **[Interruptible.](#interruptible)** Animations are cancelable by user input.
- **[Input-driven.](#input-driven)** Avoid autoplay; animate in response to actions.
- **[Correct transform origin.](#correct-transform-origin)** Anchor motion to where it “physically” starts.
- **[Never `transition: all`.](#never-transition-all)** Explicitly list only the properties you intend to animate (typically `opacity`, `transform`). `all` can unintentionally animate layout-affecting properties causing jank.
- **[Cross-browser SVG transforms.](#cross-browser-svg-transforms)** Apply CSS transforms/animations to `<g>` wrappers & set `transform-box: fill-box; transform-origin: center;`. Safari historically had bugs with transform-origin on SVG & grouping avoids origin miscalculation.
## Layout
- **[Optical alignment.](#optical-alignment)** [Adjust ±1px](https://x.com/JohnPhamous/status/1760444698857230360) when perception beats geometry.
- **[Deliberate alignment.](#deliberate-alignment)** Every element aligns with something intentionally whether to a grid, baseline, edge, or optical center. No accidental positioning.
- **[Balance contrast in lockups.](#balance-contrast-in-lockups)** When text & icons sit side by side, adjust weight, size, spacing, or color so they don’t clash. For example, a thin-stroke icon may need a bolder stroke next to medium-weight text.
- **[Responsive coverage.](#responsive-coverage)** Verify on mobile, laptop, & ultra-wide. For ultra-wide, zoom out to 50% to simulate.
- **[Respect safe areas.](#respect-safe-areas)** Account for notches & insets with [safe-area variables](https://developer.mozilla.org/en-US/docs/Web/CSS/env).
- **[No excessive scrollbars.](#no-excessive-scrollbars)** Only render useful scrollbars; fix overflow issues to prevent unwanted scrollbars. On macOS set ["Show scroll bars" to "Always"](https://support.apple.com/guide/mac-help/change-appearance-settings-mchlp1225/mac#:~:text=or%20status%20bars.-,Show%20scroll%20bars,-Scroll%20bars%20appear) to test what Windows users would see.
- **[Let the browser size things.](#let-the-browser-size-things)** Prefer flex/grid/intrinsic layout over measuring in JS. Avoid layout thrash by letting CSS handle flow, wrapping, & alignment.
## Content
- **[Inline help first.](#inline-help-first)** Prefer inline explanations; use tooltips as a last resort.
- **[Stable skeletons.](#stable-skeletons)** Skeletons mirror final content exactly to avoid layout shift.
- `<title>` reflects the current context.
- **[No dead ends.](#no-dead-ends)** Every screen offers a next step or recovery path.
- **[All states designed.](#all-states-designed)** Empty, sparse, dense, & error states.
- **[Typographic quotes.](#typographic-quotes)** Prefer curly quotes (“ ”) over straight quotes (" ").
- **[Avoid widows/orphans.](#avoid-widowsorphans)** Tidy rag & line breaks.
- **[Tabular numbers for comparisons.](#tabular-numbers-for-comparisons)** Use `font-variant-numeric: tabular-nums` or a monospace like [Geist Mono](https://vercel.com/font).
- **[Redundant status cues.](#redundant-status-cues)** Don’t rely on color alone; include text labels.
- Convey the same meaning with text for non-sighted users.
- **[Don’t ship the schema.](#dont-ship-the-schema)** Visual layouts may omit visible labels, but accessible names/labels still exist for assistive tech.
- **[Use the ellipsis character.](#use-the-ellipsis-character)** `…` over three periods `...`.
- **[Anchored headings.](#anchored-headings)** Set `scroll-margin-top` for headers when linking to sections.
- **[Resilient to user-generated content.](#resilient-to-user-generated-content)** Layouts handle short, average, & very long content.
- **[Locale-aware formats.](#locale-aware-formats)** Format dates, times, numbers, delimiters, & currencies for the user’s locale.
- **[Prefer language settings over location.](#prefer-language-settings-over-location)** Detect language via `Accept-Language` header & `navigator.languages`. Never rely on IP/GPS for language.
- **[Shield verbatim content from translation.](#shield-verbatim-content-from-translation)** Wrap brand names, product names, code tokens, & technical identifiers with `translate="no"` so browser auto-translate leaves them intact.
- **[Accessible content.](#accessible-content)** Set accurate names (`aria-label`), hide decoration (`aria-hidden`) & verify in the [accessibility tree](https://developer.chrome.com/blog/full-accessibility-tree).
- **[Icon-only buttons are named.](#icon-only-buttons-are-named)** Provide a descriptive `aria-label`.
- **[Semantics before ARIA.](#semantics-before-aria)** Prefer native elements (`button`, `a`, `label`, `table`), before `aria-*`.
- Hierarchical `<h1–h6>` & a “Skip to content” link.
- [Right-click the nav logo](https://x.com/JohnPhamous/status/1636427186566762496) to surface brand assets for quick access.
- **[Non-breaking spaces for glued terms.](#non-breaking-spaces-for-glued-terms)** Use a non-breaking space ` ` to keep units, shortcuts & names together: `10 MB` → `10 MB`, `⌘ + K` → `⌘ + K`, `Vercel SDK` → `Vercel SDK`. Use `⁠` for no space.
## Forms
- **[Enter submits.](#enter-submits)** When a text input is focused, Enter submits if it's the only control. If there are many controls, apply to the last control.
- **[Textarea behavior.](#textarea-behavior)** In `<textarea>`, ⌘/⌃+Enter submits; Enter inserts a new line.
- **[Labels everywhere.](#labels-everywhere)** Every control has a `<label>` or is associated with a label for assistive tech.
- **[Label activation.](#label-activation)** Clicking a `<label>` focuses the associated control.
- **[Submission rule.](#submission-rule)** Keep submit enabled until submission starts; then disable during the in-flight request, show a spinner, & include an idempotency key.
- **[Don’t block typing.](#dont-block-typing)** Even if a field only accepts numbers, allow any input & show validation feedback. Blocking keystrokes entirely is confusing because the user gets no explanation.
- **[Don’t pre-disable submit.](#dont-pre-disable-submit)** Allow submitting incomplete forms to surface validation feedback.
- **[No dead zones on controls.](#no-dead-zones-on-controls)** Checkboxes & radios avoid dead zones; the label & control share a single generous hit target.
- Show errors next to their fields; on submit, focus the first error.
- **[Autocomplete & names.](#autocomplete-names)** Set `autocomplete` & meaningful `name` values to enable autofill.
- **[Spellcheck selectively.](#spellcheck-selectively)** Disable for emails, codes, usernames, etc.
- **[Correct types & input modes.](#correct-types-input-modes)** Use the right `type` & `inputmode` for better keyboards & validation.
- **[Placeholders signal emptiness.](#placeholders-signal-emptiness)** End with an ellipsis.
- **[Placeholder value.](#placeholder-value)** Set placeholder to an example value or pattern e.g., `+1 (123) 456-7890` & `sk-012345679…`
- **[Unsaved changes.](#unsaved-changes)** Warn before navigation when data could be lost.
- **[Password managers & 2FA.](#password-managers-2fa)** Ensure compatibility & allow pasting one-time codes.
- **[Don’t trigger password managers for non-auth fields.](#dont-trigger-password-managers-for-non-auth-fields)** For inputs like “Search” avoid reserved names (e.g., password), use `autocomplete="off"` or a specific token like `autocomplete="one-time-code"` for OTP fields.
- **[Text replacements & expansions.](#text-replacements-expansions)** Some input methods add trailing whitespace. The input should trim the value to avoid showing a confusing error message.
- **[Windows `<select>` background.](#windows-select-background)** Explicitly set `background-color` & `color` on native `<select>` to avoid dark-mode contrast bugs on Windows.
## Performance
- **[Device/browser matrix.](#devicebrowser-matrix)** Test iOS Low Power Mode & macOS Safari.
- **[Measure reliably.](#measure-reliably)** Disable extensions that add overhead or change runtime behavior.
- **[Track re-renders.](#track-re-renders)** Minimize & make re-renders fast. Use [React DevTools](https://react.dev/learn/react-developer-tools) or [React Scan](https://react-scan.com/).
- **[Throttle when profiling.](#throttle-when-profiling)** Test with CPU & network throttling.
- **[Minimize layout work.](#minimize-layout-work)** Batch reads/writes; avoid unnecessary reflows/repaints.
- **[Network latency budgets.](#network-latency-budgets)** `POST/PATCH/DELETE` complete in <500ms.
- **[Keystroke cost.](#keystroke-cost)** Prefer uncontrolled inputs; make controlled loops cheap.
- **[Large lists.](#large-lists)** Virtualize large lists e.g., [virtua](https://github.com/inokawa/virtua) or [content-visibility: auto](https://web.dev/articles/content-visibility).
- **[Preload wisely.](#preload-wisely)** Preload only above-the-fold images; lazy-load the rest.
- **[No image-caused CLS.](#no-image-caused-cls)** Set explicit image dimensions & reserve space.
- **[Preconnect to origins.](#preconnect-to-origins)** Use `<link rel="preconnect">` for asset/CDN domains (with crossorigin when needed) to reduce DNS/TLS latency.
- **[Preload fonts.](#preload-fonts)** For critical text to avoid flash & layout shift.
- **[Subset fonts.](#subset-fonts)** Ship only the code points/scripts you use via unicode-range (limit variable axes to what you need) to shrink size.
- **[Don’t use the main thread for expensive work.](#dont-use-the-main-thread-for-expensive-work)** Move especially long tasks to [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) to avoid blocking interaction with the page.
## Design
- **[Layered shadows.](#layered-shadows)** Mimic ambient + direct light with at least two layers.
- **[Crisp borders.](#crisp-borders)** Combine borders & shadows; semi-transparent borders improve edge clarity.
- **[Nested radii.](#nested-radii)** Child radius ≤ parent radius & concentric so curves align.
- **[Hue consistency.](#hue-consistency)** On non-neutral backgrounds, tint borders/shadows/text toward the same hue.
- **[Accessible charts.](#accessible-charts)** Use color-blind-friendly palettes.
- **[Minimum contrast.](#minimum-contrast)** Prefer [APCA](https://apcacontrast.com/) over [WCAG 2](https://webaim.org/resources/contrastchecker/) for more accurate perceptual contrast.
- `:hover`, `:active`, `:focus` have more contrast than rest state.
- **[Browser UI matches your background.](#browser-ui-matches-your-background)** Set `<meta name="theme-color" content="#000000">` to [align the browser’s theme color with the page background](https://x.com/JohnPhamous/status/1816160187839107342).
- **[Set the appropriate color-scheme.](#set-the-appropriate-color-scheme)** Style the `<html>` tag with `color-scheme: dark` in dark themes so that scrollbars and other device UI have proper contrast.
- **[Text anti-aliasing & transforms.](#text-anti-aliasing-transforms)** Scaling text can change smoothing. Prefer animating a wrapper instead of the text node. If artifacts persist set `translateZ(0)` or `will-change: transform` to promote to its own layer.
- **[Avoid gradient banding.](#avoid-gradient-banding)** Fading content to dark colors using css masks can cause banding. [Background images can be used instead](https://x.com/JohnPhamous/status/1724491202148675590).
## Vercel-specific
These preferences reflect Vercel’s brand & product choices. They aren’t universal guidelines.
## Copywriting
- **[Active voice.](#active-voice)**
- Instead of “ *The CLI will be installed,”* say *“Install the CLI.”*
- **[Headings & buttons use Title Case](#headings-buttons-use-title-case)** ([Chicago](https://title.sh/)). On marketing pages, use sentence case.
- **[Be clear & concise.](#be-clear-concise)** Use as few words as possible.
- **[Prefer `&` over `and`.](#prefer-over-and)**
- **[Action-oriented language.](#action-oriented-language)**
- Instead of *“You will need the CLI…”* say *“Install the CLI…”*.
- **[Keep nouns consistent.](#keep-nouns-consistent)** Introduce as few unique terms as possible.
- **[Write in second person.](#write-in-second-person)** Avoid first person.
- **[Use consistent placeholders.](#use-consistent-placeholders)**
- Strings: `YOUR_API_TOKEN_HERE`. Numbers: `0123456789`.
- **[Use numerals for counts.](#use-numerals-for-counts)**
- Instead of *“eight deployments”* say *“8 deployments”*.
- **[Consistent currency formatting.](#consistent-currency-formatting)** In any given context, display currency with either 0 or 2 decimal places, never mix both.
- **[Separate numbers & units with a space.](#separate-numbers-units-with-a-space)**
- Instead of `10MB` say `10 MB`.
- Use a non-breaking space e.g., `10 MB`.
- **[Default to positive language.](#default-to-positive-language)** Frame messages in an encouraging, problem-solving way, even for errors.
- Instead of *“Your deployment failed,”* say *“Something went wrong—try again or contact support.”*
- Don’t just state what went wrong—tell the user how to fix it.
- Instead of *“Invalid API key,”* say *“Your API key is incorrect or expired. Generate a new key in your account settings.”* The copy & buttons/links should educate & give a clear action.
- **[Avoid ambiguity.](#avoid-ambiguity)** Labels are clear & specific.
- Instead of the button label *“Continue”* say *“Save API Key”*.
## Integrate with Agents
Use these guidelines with AI coding agents. Audit all generated interfaces.
## Review Command
Install `/web-interface-guidelines` to review UI code:
```
curl -fsSL https://vercel.com/design/guidelines/install | bash
```
Supports Antigravity, Claude Code, Cursor, Gemini CLI, OpenCode, & Windsurf.
For other agents, use the [command prompt](https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md) directly.
## AGENTS.md
Add [AGENTS.md](https://agents.md/) to your project so agents apply these guidelines during generation.
- [Download AGENTS.md](https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/AGENTS.md)
## Join Vercel
We’re hiring people who live for these details. [Check out the job postings](https://vercel.com/careers?function=Design).
---
Thanks to [Adam](https://x.com/argyleink), [Jimmy](https://x.com/wwwjim), [Jonnie](https://destroytoday.com/), [Lochie](https://x.com/lochieaxon), [Paco](https://pa.co/), [Joe](https://joebell.studio/), & [Austin](https://x.com/austin_malerba) for feedback.
Want to learn more?
Ask about this document