Imported from NexVaultX/frontend (
.opencode/skills/web-design-guidelines/SKILL.md). Install upstream withnpx skills add NexVaultX/frontend --skill web-design-guidelines. Copyright stays with the author.
Web Design Guidelines
Purpose: Review UI code for compliance with the Web Interface Guidelines and the project's design system.
How to Use Me
- Fetch the guidelines — retrieve the latest rules from Vercel Labs:
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md - Read the files — read the specified files, or ask the user which files to review.
- Apply the rules — check the files against the fetched guidelines and the project rules below.
- Output findings — use the terse
file:lineformat specified in the fetched guidelines.
Non-Negotiable Rules
1. Never Use Native UI Components When a shadcn/ui Equivalent Exists
This project uses shadcn-style components built on Base UI. When a shadcn component exists, use it — never hand-roll a native replacement.
Where the project has shadcn components available, use them for:
- Button
- Input (via
FormField) - Textarea
- Select
- Checkbox
- Switch
- Radio Group
- Dialog
- Drawer
- Sheet
- Popover
- Tooltip
- Dropdown Menu
- Context Menu
- Alert Dialog
- Tabs
- Accordion
- Avatar
- Badge
- Separator
- Card
- Scroll Area
- Skeleton
- Toast
- Progress
- Form components
Clarifications:
- Semantic HTML is still required where appropriate:
main,nav,header,section,article,aside,footer,button,a,form,label,table. These are not "native UI controls" — they are the structural backbone of an accessible page. - Native elements are allowed only when no shadcn abstraction exists
(e.g. a plain
<input>insideFormField, a native<select>when no shadcn Select exists) or when semantic HTML is required. - Never create custom replacements for existing shadcn components. If a shadcn component exists, extend or compose it — do not fork it.
- Never introduce another component library. Reuse the existing
src/components/ui/*primitives.
2. Preserve Accessibility While Improving Visuals
Every visual change must keep WCAG 2.2 AA compliance. If a change breaks a11y, it is a regression, not an improvement. See the Accessibility section below.
3. Reuse the Design System
- Use OKLCH semantic tokens (
--primary,--muted,--card,--popover,--destructive,--ring,--border,--input, etc.) — never hardcode colors. - Use the Tailwind spacing scale (
gap-*,p-*,m-*) — never arbitrary pixel values. - Use
font-headingfor display/headings, default font for body. - Use the project's motion utilities (
EASE_OUT,SPRING_*fromsrc/lib/ease.ts) andmotion/react— not CSS keyframes.
Design Tokens & Theming
- Colors: always OKLCH semantic tokens. Never raw hex, never Tailwind palette grays. Test every component in both light and dark themes.
- Surfaces: use
bg-card,bg-muted,bg-popoverfor surface differentiation — not arbitrary grays. - Text:
text-foregroundfor primary,text-muted-foregroundfor secondary/helper text. - Borders:
border-borderfor separators;border-border/70for subtler dividers inside popovers/menus. - Radius: use the token scale (
rounded-sm→rounded-4xl). Cards userounded-xl; buttons/inputs userounded-lg; pills/badges userounded-full. - Shadows: the project defines
--shadow-softand--shadow-glow, but do not add shadows speculatively — keep surfaces flat withbg-card/bg-mutedandborderfor separation unless the design explicitly calls for elevation.
Component Consistency
- Prefer shadcn/Base UI primitives over hand-rolled equivalents.
- Cards:
rounded-xl border bg-cardwithp-6padding. Hover states usehover:bg-muted/50or border emphasis — never dramatic transforms. - Buttons:
defaultfor primary actions,outlinefor secondary,ghostfor tertiary,destructivefor danger. Sizes:default,sm,lg,icon,icon-sm. - Forms: label above input, helper text below, inline validation errors
with
aria-invalid+aria-describedby. - Lists: consistent item padding (
p-3),gap-3between items,bg-muted/40item backgrounds withborder-borderborders. - Empty states: icon + title + description + optional action button.
- Loading states: skeleton placeholders or
aria-busyspinners — never blank flashes.
Visual Hierarchy
- One
<h1>per page. Use<h2>→<h3>hierarchy without skipping levels. - Page headers:
text-2xl/text-3xlheading +text-muted-foregroundsubtitle below (mt-1.5). - Section titles:
text-lg/text-xlwithfont-semibold. - Card titles:
text-sm/text-basewithfont-medium. - Use
text-muted-foregroundto de-emphasize secondary information — never rely on size alone. - Keep line lengths readable:
max-w-prosefor long-form text,max-w-2xlfor forms,max-w-7xlfor page containers.
Spacing System
- Use the Tailwind spacing scale exclusively — no arbitrary pixel values.
- Consistent section rhythm:
py-16/py-24for major sections,gap-8/gap-12between blocks. - Card padding:
p-6. List items:p-3. Dialog content:p-4withgap-4between sections. - Form fields:
grid gap-2between label/input/helper. - Button groups:
gap-2between buttons;flex-col-reverse sm:flex-row sm:justify-endfor dialog footers on mobile. - Touch targets: minimum 44×44px for interactive elements.
Typography
- Use
font-headingfor display/headings, default font for body. - Heading scale:
text-4xl/text-5xlfor hero,text-2xl/text-3xlfor section titles,text-lg/text-xlfor card titles. - Body:
text-sm/text-base; helper texttext-xs/text-smwithtext-muted-foreground. - Use
tracking-tighton large headings,text-balanceon multi-line headings. - Keep line lengths readable:
max-w-prosefor long-form text. - Never hardcode gray — use
text-muted-foreground.
Border Radius Usage
- Cards:
rounded-xl. - Buttons/inputs/selects:
rounded-lg. - Pills/badges/tags:
rounded-full. - Dialogs:
rounded-xl. - Drawers:
rounded-t-xl(bottom sheet),rounded-r-xl/rounded-l-xl(side sheet). - Avatars:
rounded-full. - Icons in boxes:
rounded-lgfor 40px boxes,rounded-mdfor smaller.
Shadows
- Default to flat surfaces:
bg-card/bg-muted+borderfor separation. - Use
--shadow-softonly when a subtle elevation is needed (e.g. floating elements). - Never add
shadow-*,drop-shadow,blur-*, or glow effects speculatively — only when the design explicitly requests them.
States
Hover States
- Subtle transitions:
transition-colors,hover:bg-muted/50. - Avoid dramatic scale/translate on hover for standard controls.
- Buttons may use
hover:-translate-y-0.5+active:scale-[0.98]for primary CTAs only, withmotion-reduce:transform-none.
Focus States
- Visible focus indicators are mandatory:
focus-visible:ring-3 focus-visible:ring-ring/50. - Never remove
outlinewithout a replacement. - Focus rings must meet 3:1 contrast against the adjacent background.
Loading States
- Skeletons (
src/components/ui/skeleton.tsx) for content regions, witharia-busy="true"on the container. - Spinners (
src/components/ui/spinner.tsx) for in-button async actions. - Route navigation: use a route
pendingComponentso client-side transitions show skeletons instead of content popping in. - Every async region must render one of: content, skeleton, empty state, or error — never nothing.
Empty States
- Icon + clear title + short description + optional action button (e.g. "Clear filters").
- Never leave a list or search area blank.
Error States
- Inline errors near the relevant UI with
role="alert". - Form validation:
aria-invalid+aria-describedbyon the input, error text below the field. - Async errors: inline alert with an actionable retry button where possible.
Destructive Actions
- Use the
destructivevariant (red) for irreversible/dangerous actions — reserve it for that purpose only. - Destructive confirmations require a Dialog with explicit warning text — never a single click.
- Use
ConfirmDialog(src/components/ui/confirm-dialog.tsx) for destructive confirmations. - In destructive dialogs: confirm button is
variant="destructive", cancel isvariant="outline". - Never use destructive styling for reversible actions.
Animations & Motion
- Keep animations under 400ms for UI interactions.
- Use
transform/opacityonly — never animatewidth/height/top/left. - Use
motion/react(LazyMotion+domAnimation) for scroll reveals and page transitions — not CSS keyframes. - Use the project's easing constants from
src/lib/ease.ts(EASE_OUT,EASE_IN_OUT,EASE_DRAWER) and spring configs (SPRING_PRESS,SPRING_SWAP,SPRING_PANEL,SPRING_LAYOUT). - Respect
prefers-reduced-motion— disable or minimize all animations (useReducedMotion()frommotion/react). - Don't create flashing content (WCAG 2.3.1 — no more than 3 flashes per second).
Accessibility
- Semantic HTML: use
main,nav,header,section,article,aside,footer,button,a,form,label,table. Neverdivsoup. - Landmarks: exactly one
<main>per page;<nav aria-label="...">when multiple nav regions exist;<section aria-labelledby="...">to associate sections with headings. - Keyboard: all interactive elements reachable and operable via keyboard (Tab, Enter, Space, Arrow keys). Never trap focus — dialogs must close on Esc and return focus to the trigger.
- ARIA: prefer native semantics over ARIA. Use
aria-labelonly when there's no visible text label. Usearia-livefor dynamic content updates. Usearia-describedbyto associate helper text with inputs. - Forms: every input needs a visible
<label>(oraria-labelif icon-only). Usetypecorrectly (email,password,search, etc.). Useautocompleteattributes where appropriate. - Contrast: text ≥ 4.5:1 (WCAG AA), large text ≥ 3:1, UI components (borders, icons, focus rings) ≥ 3:1. Test both themes.
- Images: meaningful
alttext;alt=""for decorative images. - Reduced motion: all animations must respect
prefers-reduced-motion: reduce. - Test: keyboard-only, screen reader (NVDA/VoiceOver/axe), contrast at all breakpoints and in both themes.
Mobile-First Responsiveness
- Design for small screens first, enhance with
sm:/md:/lg:breakpoints. - Touch targets ≥ 44×44px on mobile.
- Use
lg:hidden/hidden lg:flexfor responsive nav/menus — never JS-based breakpoint detection. - Dialog footers:
flex-col-reverse gap-2 sm:flex-row sm:justify-end. - Test at 320px, 375px, 768px, 1024px, and 1440px.
Dialog/Drawer Usage
- Use
Dialog(src/components/ui/dialog.tsx) for centered modals. - Use
Drawer(src/components/ui/drawer.tsx) for bottom sheets on mobile and side sheets on desktop. - Structure:
DialogHeader(title + description) → content →DialogFooter(actions). - Titles:
DialogTitlewithfont-heading text-base font-medium. - Descriptions:
DialogDescriptionwithtext-muted-foreground text-sm. - Footers:
DialogFooterwithflex-col-reverse gap-2 sm:flex-row sm:justify-end; primary action last (rightmost on desktop, topmost on mobile). - Destructive: confirm button
variant="destructive", cancelvariant="outline". - Close:
showCloseButtononDialogContentfor the X button; Esc to close; focus returns to trigger on close. - Scroll:
overscroll-containon content; keep the dialog withinmax-w-[calc(100%-2rem)]on mobile. - Backdrop:
bg-black/10with fade-in/out; never fully opaque. - Elevation: dialogs sit at
z-50above all page content.
Icon Usage
- Use
@tabler/icons-react— the project's icon set. - Consistent stroke:
stroke={1.8}for UI icons,stroke={2}for small inline badges. - Sizes:
size={16}for menu items,size={18}for list item icons,size={20}+ for feature icons. - Decorative icons:
aria-hidden="true". - Icon-only buttons:
sr-onlylabel oraria-label. - Never mix icon sets.
Color Usage
- Always OKLCH semantic tokens — never raw hex.
--primary(green) for primary actions and active states.--destructive(red) for irreversible/dangerous actions only.--muted-foregroundfor secondary/helper text.--muted/--card/--popoverfor surface differentiation.- Never rely on color alone to convey meaning — pair color with icons, text, or patterns.
- Test both light and dark themes — contrast that passes in one may fail in the other.
Surface/Background Layering
- Page background:
bg-background. - Cards:
bg-cardwithborder-border— the default content surface. - Muted regions:
bg-muted/bg-muted/40for list items, secondary regions, and footer areas. - Popovers/menus/dialogs:
bg-popoverwithborder-borderandring-1 ring-foreground/10for elevation. - Overlays:
bg-black/10for dialog/drawer backdrops. - Keep layering flat — use borders and subtle background shifts, not shadows, to separate surfaces.
Loading Skeletons and Empty States
Always check for loading and empty states when reviewing a page:
- Loading skeletons: while data is loading, show skeleton placeholders
(see
src/components/ui/skeleton.tsx) instead of a blank flash or content popping in. Mark the containeraria-busy="true". For route navigation, use a routependingComponentso client-side transitions show skeletons instead of the previous page lingering or content popping in. - Empty states: when a list or search returns no results, show an empty state with an icon, a clear title, a short description, and an optional action button (e.g. "Clear filters"). Never leave the area blank.
- Error states: show inline errors near the relevant UI with
role="alert"and an actionable retry where possible. - No blank flashes: every async region must render one of: content, skeleton, empty state, or error — never nothing.
Review Checklist
When reviewing UI, check in this order:
- shadcn-only rule — native controls replaced with shadcn equivalents?
- Semantic HTML & landmarks — one
<main>, proper<section>/<nav>/<header>usage, heading hierarchy? - Design tokens — OKLCH tokens only, no hardcoded colors/grays?
- Spacing & radius — Tailwind scale, consistent
rounded-*usage? - States — hover, focus, loading, empty, error all handled?
- Destructive actions — confirm dialogs, destructive variant only for danger?
- Motion — under 400ms, transform/opacity only, reduced-motion respected?
- Accessibility — keyboard nav, focus visibility, ARIA, contrast, screen-reader labels?
- Responsive — mobile-first, 44px touch targets, breakpoint behavior?
- Consistency — matches existing components, no parallel implementations?
Tips
- Always fetch fresh guidelines before each review — the rules change over time
- Use the terse
file:lineoutput format for findings - Ask the user which files to review if none are specified
- When a finding has a fix, suggest the exact shadcn component or token to use