Web Interface Guidelines
Review and enforce Vercel's web interface standards across UI code—accessibility, performance, visual stability, animation, forms, and content handling.
When to Use
- Building or reviewing any web UI component (React, Vue, plain HTML/CSS).
- Auditing markup, styles, or JavaScript logic for accessibility or performance regressions.
- Adding images, fonts, lists, forms, or animations to a web interface.
- Triggered by keywords: UI review, accessibility audit, CLS, focus state, reduced motion, form input, virtualization, image dimensions, font preload.
Do Not Use
- Non-web platforms (native mobile, desktop apps).
- Legacy codebases where these guidelines require a full migration plan—prioritize critical security and accessibility fixes first, then adopt incrementally.
- Enforcing arbitrary stylistic preferences that do not impact performance, accessibility, or visual stability.
Prerequisites
- Familiarity with the target framework (React, Vue, Next.js, etc.).
- Access to browser DevTools (Lighthouse, Network, Performance tabs).
- Optional linting tools:
eslint-plugin-jsx-a11y, Stylelint, Lighthouse CI, Axe Core.
Procedure
- Scan component markup for image, form, and semantic HTML rules (see Quick Reference below).
- Scan styles for focus states, animation properties, and content handling rules.
- Scan JavaScript logic for layout reads in render, virtualization needs, and re-render optimization.
- Flag violations and suggest the appropriate fix, referencing the specific guideline.
- Run automated tooling to catch common issues:
npx eslint --ext .js,.jsx,.ts,.tsx .witheslint-plugin-jsx-a11yenabled.npx lighthouse http://localhost:3000 --only-categories=accessibility,performancefor key pages.- Axe Core browser extension or
@axe-core/clifor deeper accessibility audits.
- Document intentional deviations with a justification comment in the code explaining trade-offs or context.
Quick Reference — Visual Stability
| Issue | Rule |
|---|---|
| Images without dimensions | <img> needs explicit width and height (prevents CLS) |
| Font loading flash | Critical fonts: <link rel="preload" as="font" crossorigin> with font-display: swap |
| Large lists | Virtualize lists >50 items (react-virtual, vue-virtual-scroller, content-visibility: auto) |
| Layout reads in render | No getBoundingClientRect, offsetHeight in render path; use requestAnimationFrame for DOM reads/writes |
Full Rules
Images
<img>needs explicitwidthandheightattributes or CSSaspect-ratio(prevents CLS). Useaspect-ratiofor responsive images.- Below-fold images:
loading="lazy". - Above-fold critical images:
fetchpriority="high"orpriorityattribute (Next.js Image component). - Use modern formats (WebP, AVIF) with
<picture>and<source>. - Ensure
srcsetandsizesare correctly used for responsive delivery.
Performance
- Large lists (>50 items): virtualize with
@tanstack/react-virtual,vue-virtual-scroller, orcontent-visibility: autofor non-interactive sections. - No layout reads in render (
getBoundingClientRect,offsetHeight,offsetWidth,scrollTop,scrollWidth,scrollHeight). Batch DOM reads/writes viarequestAnimationFrameorIntersectionObserver. - Add
<link rel="preconnect">for CDN/asset domains;<link rel="dns-prefetch">for older browsers. - Critical fonts:
<link rel="preload" as="font" type="font/woff2" crossorigin>withfont-display: swaporoptional. Use WOFF2. - Minimize JS bundle: tree-shaking, code-splitting (dynamic imports), ESM.
- Optimize CSS: inline critical CSS, lazy-load non-critical. Prefer CSS-in-JS with atomic CSS or static extraction.
- Avoid excessive re-renders:
React.memo,useMemo,useCallback, or Vue reactivity transforms.
Accessibility
- Icon-only buttons need
aria-labelor visually hidden text. - Form controls need
<label>viahtmlFororaria-labelledby, oraria-labelfor standalone controls. - Interactive elements need keyboard handlers (
onKeyDown/onKeyUp); ensureTab,Enter,Spacework. - Use native elements:
<button>for actions,<a>/<Link>for navigation. Avoid<div>/<span>withonClick; if unavoidable, addrole="button"and keyboard handlers. - Images need meaningful
alt(oralt=""if decorative). Complex images:aria-describedbypointing to longer description. - Ensure color contrast (WCAG 2.1 AA or AAA).
- Provide clear focus indicators for all interactive elements.
- Use semantic HTML5 (
<nav>,<main>,<aside>,<article>,<section>,<footer>,<header>). - Implement ARIA live regions (
aria-live="polite") for dynamic content updates.
Focus States
- Interactive elements need visible focus:
focus-visible:ring-*,focus-visible:outline-*, or equivalent. - Never use
outline-none/outline: nonewithout a clear, accessible focus indicator replacement. - Use
:focus-visibleover:focusto prevent focus rings on click interactions. - Ensure focus order follows visual order.
Animation
- Honor
prefers-reduced-motion(e.g.,useReducedMotionfromframer-motion). Provide reduced motion variant or disable animations. - Animate only
transform(translate,scale,rotate) andopacity—compositor-friendly. Avoid animatingwidth,height,margin,padding,top,left. - Never use
transition: all—list properties explicitly. - Use
will-changejudiciously; remove when animation completes to avoid memory overhead. - For complex animations:
Framer Motion,React Spring, orGSAP.
Forms
- Inputs need
autocompleteattributes (e.g.,autocomplete="email",autocomplete="current-password"). - Use meaningful
nameattributes. - Use correct
type(email,tel,url,number,date,time,search) andinputmode(e.g.,inputmode="numeric") for mobile keyboards. - Never block paste (
onPaste+preventDefault) unless critical security reason; provide clear feedback if blocked. - Labels must be clickable and associated via
htmlForor by wrapping the control. - Implement client-side and server-side validation with clear error messages. Use
aria-invalidandaria-describedby.
Content Handling
- Text containers handle long content:
text-overflow: ellipsis+overflow: hidden+white-space: nowrapfor single lines;line-clamp-*for multi-line;word-break: break-word/overflow-wrap: break-wordfor wrapping. - Flex children need
min-w-0(ormin-width: 0) to allow truncation within flex containers. - Handle empty states gracefully—no broken UI for empty strings/arrays. Provide "no data" messages or placeholders.
- Use responsive typography:
clamp(),remunits, media queries.
Pitfalls
user-scalable=noormaximum-scale=1in viewport meta—critical accessibility violation. Always flag.transition: allwithout explicit property listing—causes unintended performance issues.outline-none/outline: nonewithout:focus-visiblereplacement—removes keyboard accessibility.- Images without
width/heightoraspect-ratio—causes CLS. - Large arrays
.map()without virtualization for lists >~50 items—performance degradation. - Form inputs without
<label>oraria-label—accessibility failure. - Icon buttons without
aria-label—unusable by screen readers. <div>/<span>withonClickinstead of<button>/<a>—missing keyboard support and semantics.- Inline styles for critical layout that should be CSS classes—maintainability and performance issues.
- Excessive
!importantin CSS—specificity wars and maintainability issues. - Blocking main thread with long-running JS—use
requestIdleCallbackor Web Workers. - Animating layout properties (
width,height,margin,padding,top,left)—triggers layout and paint thrashing.
Examples
// Correct image usage with modern attributes and aspect-ratio
<img
src="/hero.webp"
alt="A vibrant sunset over a calm ocean"
width={1920}
height={1080}
loading="lazy"
fetchpriority="high"
style={{ aspectRatio: '16 / 9', objectFit: 'cover' }}
/>
// Accessible button with aria-label and focus-visible
<button
type="button"
aria-label="Close dialog"
className="p-2 rounded-full focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
<svg /* ... close icon ... */ />
</button>
// Virtualized list using @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import React from 'react';
function MyVirtualizedList({ items }) {
const parentRef = React.useRef();
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: `400px`, overflow: 'auto' }}>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
}
// Accessible form input with label and autocomplete
<div className="form-group">
<label htmlFor="email-input" className="block text-sm font-medium text-gray-700">
Email Address
</label>
<input
type="email"
id="email-input"
name="email"
autoComplete="email"
placeholder="you@example.com"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
aria-required="true"
/>
</div>
// CSS for reduced motion
.animated-element {
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.animated-element {
transition: none;
animation: none;
}
}
Verification
- Run Lighthouse on key pages—Accessibility, Performance, and SEO scores above 90:
npx lighthouse http://localhost:3000 --only-categories=accessibility,performance,seo --view - Verify all
<img>tags have explicitwidthandheightoraspect-ratioCSS. - Manual keyboard navigation test: all interactive elements reachable and operable via
Tab,Shift+Tab,Enter,Space. - Screen reader test (NVDA, VoiceOver, JAWS): interactive elements and dynamic content announced correctly.
- Activate
prefers-reduced-motionin OS settings—animations disabled or replaced with subtle alternative. - Check DevTools console for layout thrashing warnings or performance bottlenecks during interactions.
- Review Network tab: critical fonts preloaded, modern image formats (WebP/AVIF) served.
- Validate form inputs: correct
type,autocomplete, associated<label>oraria-label. - Run ESLint with jsx-a11y plugin:
npx eslint --ext .js,.jsx,.ts,.tsx . --rule '{"jsx-a11y/alt-text":"error","jsx-a11y/anchor-is-valid":"error"}'
Related Skills
senior-frontend— Next.js/React scaffold, component generators, bundle analyzer scripts. This chair does not scaffold apps.emil-design-eng— animation craft and interaction taste (easing, transform-origin, springs). This chair enforces Vercel guideline compliance, not motion direction.- Run the execute path here:
npx eslint+jsx-a11y,npx lighthouse, keyboard/reduced-motion checks.