Code-level UI rules (Vercel guidelines mapped to UX-n): accessibility, focus, forms, motion, performance. Use for «проверь вёрстку», «ревью вёрстки». Not mockups (ux-patterns) or the running app (ui-test-cases).
Resources
3Install
npx skillscat add idontlikedrinkpivo/web-dev-pipeline-codex-plugin/web-design-guidelines Install via the SkillsCat registry.
Codex runtime. Before using this skill, read
../executor-catalog/references/codex-runtime.md. It defines tool mappings, named-agent dispatch and stage-chat orchestration; use it for inherited Claude Code terms below.
Web interface guidelines
The code-level form of ux-patterns: what each rule looks like in markup,
CSS and React. Every rule names the ux-patterns rule it belongs to (the
Next.js hydration rules belong to frontend's profile), so a finding here
and a finding against the frames cite the same id. An
embedded copy of Vercel's Web Interface Guidelines (MIT,vercel-labs/web-interface-guidelines, commit 434b7f9); LICENSE is
the original licence and VENDORED.md lists what changed.
Who uses it:
frontendandimpl-uiwrite screen code by these rules and, before
handing a screen unit over, review their own diff with them together withux-patterns→ «Проверка экрана перед сдачей».code-review-unitchecks a screen unit's diff against them (its UI lens)
and reports in the format below.- Directly: the user names files or a folder to check. With no files named,
ask which ones. These rules read code: «проверь интерфейс» about mockups
in Figma goes toux-patterns(its rules and «Полнота макета»), about
the running app toui-test-cases(mode run).
The rules are here in full; do not fetch them from the network. The
upstream file can hold rules that are not yet mapped to ux-patterns, and
one of them could contradict a decision this pipeline made (placeholders,
quotes, focus after a failed submit).
How to review
- Run
python3 -I "<folder of this SKILL.md>/scripts/flag_antipatterns.py" <files or folders in scope>first. It finds the breaches a text search
can (Anti-patterns below) and prints them in the report format. Each
line is a candidate: open it and keep it only when the rule holds there. - Read the files in scope — for a unit, the files its diff touches.
- Check every rule below that the code can break. A rule marked
«только по требованиям» applies only when the SRS asks for that
feature; a Next.js rule only in a Next.js project. - Report each breach once, at the line that causes it, with its
UX-n.
Say what to change when the fix is not obvious from the finding.
Rules
Accessibility
- Icon-only buttons have an accessible name (
aria-label) → UX-8 - Form controls have a visible
<label>→ UX-17 - Custom interactive elements handle the keyboard (
onKeyDown/onKeyUp); native<button>and<a>already do → UX-64 <button>for actions,<a>/<Link>for navigation, never<div onClick>→ UX-64- Images have
alt; a decorative image hasalt=""→ UX-66 - Decorative icons have
aria-hidden="true"→ UX-66 - Toasts and field messages are announced politely (
aria-live="polite"); a field error is tied to its field witharia-describedby;role="alert"only for a failed operation → UX-28 - Semantic HTML (
<button>,<a>,<label>,<table>) before ARIA → UX-64 - Headings in order
<h1>–<h6>→ UX-3; a skip link to the main content on pages with navigation → UX-64 scroll-margin-topon heading anchors → UX-65- Meaningful media has captions, a transcript or a description → UX-66
- Media controls work from the keyboard; decorative media is hidden from assistive technology → UX-61, UX-66
Focus
- Every interactive element has a visible focus:
focus-visible:ring-*or equivalent → UX-63 - Never
outline-none/outline: nonewithout a replacement → UX-63 :focus-visiblerather than:focus(no ring on click) → UX-63:focus-withinfor compound controls → UX-63- Sticky headers, footers and overlays do not cover the focused element → UX-65
- A dialog traps Tab, closes on Esc and returns focus to its trigger; while its own request runs, Esc, the
close button and an outside click do not close it, so a refusal is shown where the user is looking → UX-34
Forms
- Inputs have
autocompleteand a meaningfulname→ UX-19 - The right
type(email,tel,url) andinputmode(numeric,decimal); nevertype="number"→ UX-19 - Paste is never blocked (
onPaste+preventDefault) → UX-19 - A click on the label focuses the field (
htmlForor a wrapping label) → UX-69 spellCheck={false}on emails, codes and usernames → UX-19- A checkbox or radio and its label share one hit target, no dead zone → UX-69
- The submit button stays enabled until the request starts; during the request it shows a busy state and ignores repeat presses, without
disabled(which drops focus) → UX-14, UX-15 - A field's error sits under that field → UX-21; after a failed submit focus goes to the error summary at the top, and a one-field form keeps focus on its field → UX-22
- No placeholder, except in a search field («Поиск по названию…»); a format example goes in the hint between the label and the field → UX-17, UX-19
autocomplete="off"where browser autofill gets in the way (search, internal codes) → UX-19- Leaving with unsaved changes asks first (
beforeunloador a router guard) → UX-35
Motion
The motion tokens — one set for every product, defined once in the theme (CSS variables) and used everywhere:
| Token | Value | For |
|---|---|---|
--dur-press |
120 ms | a press, a toggle, a checkbox |
--dur-tooltip |
150 ms | a tooltip, a small popover |
--dur-menu |
200 ms | a dropdown, a menu, a select |
--dur-dialog |
250 ms | a dialog, a toast; a side panel up to 350 ms |
| exit | about two thirds of the entry | every closing |
--ease-out |
cubic-bezier(0.23, 1, 0.32, 1) |
entering and leaving |
--ease-in-out |
cubic-bezier(0.77, 0, 0.175, 1) |
moving on screen |
linear |
— | loading indicators only |
- A duration or a curve written in place of a token;
ease-inanywhere; a UI animation over 350 ms → UX-61 prefers-reduced-motionremoves movement — translate, scale, parallax, smooth scrolling — and keeps opacity and colour transitions; loading indicators keep moving → UX-61- Not animated: what the user does many times a minute (list navigation, typing feedback) and every keyboard action (a shortcut, the command palette) → UX-61
- A pressed button may use
scale(0.97)for--dur-press; nothing appears fromscale(0)— start atscale(0.95)with opacity 0 → UX-61 - Animate
transformandopacityonly → UX-61 - Never
transition: all; list the properties → UX-61 - A popover grows from the control that opened it (
transform-originat its trigger); a dialog from the centre; in SVG, transforms on a<g>withtransform-box: fill-box; transform-origin: center→ UX-61 - Tooltips: a delay before the first; while one is open, the next appears at once → UX-61
- What is triggered often and interrupted (a toast, a switch) uses CSS transitions, not keyframes, so it reverses from where it is → UX-61
- Hover effects only inside
@media (hover: hover) and (pointer: fine)→ UX-63 - Animations stop or reverse when the user acts; they never block input → UX-61
- Anything that moves on its own for more than 5 s has pause, stop or hide → UX-61
- Muted decorative loops stop under
prefers-reduced-motion→ UX-61 - An overlay inside an
overflow: hiddencontainer escapes it through<dialog>, the popover API or a portal → UX-61
Typography
- An ellipsis is one character,
…, not...→ UX-78 - Quotes «ёлочки», inner quotes „лапки“; no straight
"in interface text → UX-78 - A non-breaking space between a number and its unit (
10 МБ,5 мин), in№ 5,т. е., in shortcuts (⌘ K) → UX-78 - Waiting states end with
…: «Загрузка…», «Сохранение…» → UX-78 font-variant-numeric: tabular-numsfor number columns, totals and timers → UX-59text-wrap: balance(orpretty) on short multi-line headings → UX-59- Every font loads with its
cyrillicsubset (next/font:subsets: ['latin', 'cyrillic'];@fontsource: the cyrillic files) → UX-59
Long content
- Text containers handle long content:
break-words(overflow-wrap: anywherefor links and ids), ortruncate/line-clamp-*for secondary text only — never for actions, errors, names or titles — with the full text reachable without hover → UX-71 - Flex children that hold text have
min-w-0→ UX-71 - Empty strings and arrays render the screen's empty state, not a broken layout → UX-44
- The screen holds short, average and very long values → UX-71
Images
<img>has explicitwidthandheight→ UX-75- Images below the first screen:
loading="lazy"→ UX-75 - The critical image of the first screen:
priorityorfetchpriority="high"→ UX-48
Performance
- Lists over ~50 items are virtualized (
virtua,content-visibility: auto) or paginated → UX-75, UX-43 - No layout reads in render (
getBoundingClientRect,offsetHeight,offsetWidth,scrollTop) → UX-75 - DOM reads and writes are batched, not interleaved → UX-75
- Prefer uncontrolled inputs; keep a controlled input cheap per keystroke → UX-75
<link rel="preconnect">for CDN and asset domains → UX-75- Critical fonts:
<link rel="preload" as="font">withfont-display: swap→ UX-75 - A muted looping
<video autoplay muted loop playsinline>instead of an animated GIF, with a still alternative; for short loops, an H.264 MP4 source for Safari and aprefers-reduced-motionstill → UX-75, UX-61
Navigation and state
- The URL holds the state: filters, tabs, pagination, expanded panels → UX-32
- Links are
<a>/<Link>(Cmd/Ctrl+click and middle click work) → UX-64 - Every stateful view can be linked to (state in the URL, e.g. with nuqs) → UX-32
- A destructive action has a confirmation or an undo window, never runs at once → UX-36
Touch and interaction
touch-action: manipulation→ UX-76-webkit-tap-highlight-colorset on purpose → UX-76overscroll-behavior: containin modals, drawers and sheets → UX-74- Page rubber-band only with a fine pointer:
overscroll-behavior: noneon<html>inside@media (pointer: fine); coarse pointers keep the page default; check hybrid touch devices → UX-74 - While dragging: no text selection,
inerton the dragged elements → UX-76 - Drag, swipe, pinch and path gestures have a tap or click and a keyboard alternative → UX-67
autoFocusonly on desktop and only for the single main field → UX-76- Zoom is never disabled: no
user-scalable=no, nomaximum-scale=1→ UX-55
Layout
- Full-bleed layouts respect
env(safe-area-inset-*)→ UX-74 - No unwanted scrollbars: fix the overflow; the page never scrolls sideways (a table scrolls inside itself) → UX-74
- Layout with flex and grid, not JS measurement → UX-74
Theming
color-scheme: darkon<html>in a dark theme → UX-62<meta name="theme-color">matches the page background → UX-62- Native
<select>has an explicitbackground-colorandcolor→ UX-62
Locale
- Dates and times through
Intl.DateTimeFormat, not hand-made formats → UX-77 - Numbers and money through
Intl.NumberFormat→ UX-77 - The interface language from
Accept-Language/navigator.languages, never from the IP → только по требованиям (a multilingual product) - Brand names, code tokens and identifiers carry
translate="no"→ UX-77
Next.js hydration
Only in a Next.js project; the details are in frontend →references/nextjs.md.
- An input with
valuehasonChange, or usesdefaultValue - Dates and times rendered on the server and the client do not mismatch
suppressHydrationWarningonly where it is truly needed
Hover and states
- Buttons and links have a hover state → UX-63
- Hover, pressed and focus raise contrast over the rest state; non-interactive cards do not react to hover → UX-63
Copy
- Active voice: «Установите CLI», not «CLI будет установлен» → UX-79
- A capital letter only on the first word of a heading or button → UX-12, UX-78
- Counts in digits: «8 развёртываний» → UX-78
- Specific button labels: «Сохранить ключ», not «Продолжить» → UX-12
- An error message says how to fix it, not only what went wrong → UX-21
- The user is «вы», in lower case; sections are named without pronouns («Проекты», not «Мои проекты»), «мои» only where others' items are shown too → UX-79
Anti-patterns — the script
scripts/flag_antipatterns.py searches for the breaches of the rules above
that show in the text: zoom disabled, paste blocked, type="number",transition: all, outline removed with no replacement, <div> or<span> with onClick, navigation by onClick, <img> without size, a
GIF, autoFocus, a date built by hand. What it cannot see — a long list
without virtualization, an icon button's name, a field's label, a
gesture-only action — is found by reading, under the rules above.
Report format
Group by file, one line per finding, file:line — UX-n — суть, in Russian,
no preamble. A file with nothing to report gets ✓ замечаний нет.
## src/features/bookings/BookingForm.tsx
src/features/bookings/BookingForm.tsx:42 — UX-8 — у кнопки-иконки «Удалить» нет aria-label
src/features/bookings/BookingForm.tsx:67 — UX-61 — transition: all → перечислить свойства
src/features/bookings/BookingForm.tsx:88 — UX-78 — "..." → «…»
## src/components/RoomCard.tsx
✓ замечаний нетInside code-review-unit the same lines become that skill's findings, with
its own severity and verdict.