Phase 1: Session log archived (1096→91 lines), D031 token access convention
Phase 2: ESLint v9 + Prettier + jsx-a11y, initial config and lint fixes
Phase 3: 7 new skills (polish, harden, normalize, clarify, typeset, quieter, adapt)
+ Vercel reference docs, updated audit/review-component refs
Phase 4: Husky + lint-staged pre-commit hooks, preflight updated to 8 checks
Phase 5: Vitest + Testing Library + /write-tests skill
- Badge.tsx colour maps unified to CSS variables (D031)
- 5 empty interface→type alias fixes (Switch, Radio, Divider, IconButton, Link)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
6.7 KiB
Markdown
180 lines
6.7 KiB
Markdown
---
|
||
description: Review UI code for Vercel Web Interface Guidelines compliance
|
||
argument-hint: <file-or-pattern>
|
||
---
|
||
|
||
# Web Interface Guidelines
|
||
|
||
Review these files for compliance: $ARGUMENTS
|
||
|
||
Read files, check against rules below. Output concise but comprehensive—sacrifice grammar for brevity. High signal-to-noise.
|
||
|
||
## Rules
|
||
|
||
### Accessibility
|
||
|
||
- Icon-only buttons need `aria-label`
|
||
- Form controls need `<label>` or `aria-label`
|
||
- Interactive elements need keyboard handlers (`onKeyDown`/`onKeyUp`)
|
||
- `<button>` for actions, `<a>`/`<Link>` for navigation (not `<div onClick>`)
|
||
- Images need `alt` (or `alt=""` 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-top` on heading anchors
|
||
|
||
### Focus States
|
||
|
||
- Interactive elements need visible focus: `focus-visible:ring-*` or equivalent
|
||
- Never `outline-none` / `outline: none` without focus replacement
|
||
- Use `:focus-visible` over `:focus` (avoid focus ring on click)
|
||
- Group focus with `:focus-within` for compound controls
|
||
|
||
### Forms
|
||
|
||
- Inputs need `autocomplete` and meaningful `name`
|
||
- Use correct `type` (`email`, `tel`, `url`, `number`) and `inputmode`
|
||
- Never block paste (`onPaste` + `preventDefault`)
|
||
- Labels clickable (`htmlFor` or 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 (`beforeunload` or router guard)
|
||
|
||
### Animation
|
||
|
||
- Honor `prefers-reduced-motion` (provide reduced variant or disable)
|
||
- Animate `transform`/`opacity` only (compositor-friendly)
|
||
- Never `transition: all`—list properties explicitly
|
||
- Set correct `transform-origin`
|
||
- SVG: transforms on `<g>` wrapper with `transform-box: fill-box; transform-origin: center`
|
||
- Animations interruptible—respond to user input mid-animation
|
||
|
||
### Typography
|
||
|
||
- `…` not `...`
|
||
- Curly quotes `"` `"` not straight `"`
|
||
- Non-breaking spaces: `10 MB`, `⌘ K`, brand names
|
||
- Loading states end with `…`: `"Loading…"`, `"Saving…"`
|
||
- `font-variant-numeric: tabular-nums` for number columns/comparisons
|
||
- Use `text-wrap: balance` or `text-pretty` on headings (prevents widows)
|
||
|
||
### Content Handling
|
||
|
||
- Text containers handle long content: `truncate`, `line-clamp-*`, or `break-words`
|
||
- Flex children need `min-w-0` to allow text truncation
|
||
- Handle empty states—don't render broken UI for empty strings/arrays
|
||
- User-generated content: anticipate short, average, and very long inputs
|
||
|
||
### Images
|
||
|
||
- `<img>` needs explicit `width` and `height` (prevents CLS)
|
||
- Below-fold images: `loading="lazy"`
|
||
- Above-fold critical images: `priority` or `fetchpriority="high"`
|
||
|
||
### Performance
|
||
|
||
- Large lists (>50 items): virtualize (`virtua`, `content-visibility: auto`)
|
||
- No layout reads in render (`getBoundingClientRect`, `offsetHeight`, `offsetWidth`, `scrollTop`)
|
||
- Batch DOM reads/writes; avoid interleaving
|
||
- Prefer uncontrolled inputs; controlled inputs must be cheap per keystroke
|
||
- Add `<link rel="preconnect">` for CDN/asset domains
|
||
- Critical fonts: `<link rel="preload" as="font">` with `font-display: swap`
|
||
|
||
### Navigation & State
|
||
|
||
- URL reflects state—filters, tabs, pagination, expanded panels in query params
|
||
- Links use `<a>`/`<Link>` (Cmd/Ctrl+click, middle-click support)
|
||
- Deep-link all stateful UI (if uses `useState`, consider URL sync via nuqs or similar)
|
||
- Destructive actions need confirmation modal or undo window—never immediate
|
||
|
||
### Touch & Interaction
|
||
|
||
- `touch-action: manipulation` (prevents double-tap zoom delay)
|
||
- `-webkit-tap-highlight-color` set intentionally
|
||
- `overscroll-behavior: contain` in modals/drawers/sheets
|
||
- During drag: disable text selection, `inert` on dragged elements
|
||
- `autoFocus` sparingly—desktop only, single primary input; avoid on mobile
|
||
|
||
### Safe Areas & Layout
|
||
|
||
- Full-bleed layouts need `env(safe-area-inset-*)` for notches
|
||
- Avoid unwanted scrollbars: `overflow-x-hidden` on containers, fix content overflow
|
||
- Flex/grid over JS measurement for layout
|
||
|
||
### Dark Mode & Theming
|
||
|
||
- `color-scheme: dark` on `<html>` for dark themes (fixes scrollbar, inputs)
|
||
- `<meta name="theme-color">` matches page background
|
||
- Native `<select>`: explicit `background-color` and `color` (Windows dark mode)
|
||
|
||
### Locale & i18n
|
||
|
||
- Dates/times: use `Intl.DateTimeFormat` not hardcoded formats
|
||
- Numbers/currency: use `Intl.NumberFormat` not hardcoded formats
|
||
- Detect language via `Accept-Language` / `navigator.languages`, not IP
|
||
|
||
### Hydration Safety
|
||
|
||
- Inputs with `value` need `onChange` (or use `defaultValue` for uncontrolled)
|
||
- Date/time rendering: guard against hydration mismatch (server vs client)
|
||
- `suppressHydrationWarning` only where truly needed
|
||
|
||
### Hover & Interactive States
|
||
|
||
- Buttons/links need `hover:` state (visual feedback)
|
||
- Interactive states increase contrast: hover/active/focus more prominent than rest
|
||
|
||
### Content & Copy
|
||
|
||
- Active voice: "Install the CLI" not "The CLI will be installed"
|
||
- Title Case for headings/buttons (Chicago style)
|
||
- Numerals for counts: "8 deployments" not "eight"
|
||
- Specific button labels: "Save API Key" not "Continue"
|
||
- Error messages include fix/next step, not just problem
|
||
- Second person; avoid first person
|
||
- `&` over "and" where space-constrained
|
||
|
||
### Anti-patterns (flag these)
|
||
|
||
- `user-scalable=no` or `maximum-scale=1` disabling zoom
|
||
- `onPaste` with `preventDefault`
|
||
- `transition: all`
|
||
- `outline-none` without focus-visible replacement
|
||
- Inline `onClick` navigation without `<a>`
|
||
- `<div>` or `<span>` with click handlers (should be `<button>`)
|
||
- Images without dimensions
|
||
- Large arrays `.map()` without virtualization
|
||
- Form inputs without labels
|
||
- Icon buttons without `aria-label`
|
||
- Hardcoded date/number formats (use `Intl.*`)
|
||
- `autoFocus` without clear justification
|
||
|
||
## Output Format
|
||
|
||
Group by file. Use `file:line` format (VS Code clickable). Terse findings.
|
||
|
||
```text
|
||
## src/Button.tsx
|
||
|
||
src/Button.tsx:42 - icon button missing aria-label
|
||
src/Button.tsx:18 - input lacks label
|
||
src/Button.tsx:55 - animation missing prefers-reduced-motion
|
||
src/Button.tsx:67 - transition: all → list properties
|
||
|
||
## src/Modal.tsx
|
||
|
||
src/Modal.tsx:12 - missing overscroll-behavior: contain
|
||
src/Modal.tsx:34 - "..." → "…"
|
||
|
||
## src/Card.tsx
|
||
|
||
✓ pass
|
||
```
|
||
|
||
State issue + location. Skip explanation unless fix non-obvious. No preamble.
|