A single, holistic skill for delivering web interfaces that look unforgettable, perform near the top of the Lighthouse curve, and degrade gracefully across devices and assistive technologies. Framework-agnostic by design: every rule is expressed as a principle plus a concrete check, never as a library API.
When to Use
Apply this skill any time the work changes how an interface looks, feels, moves, loads, ranks, or is interacted with. Examples:
- Building or refactoring any page, form, modal, navigation, table, chart, or component
- Choosing typography, color, spacing, motion, or visual style
- Auditing or improving Core Web Vitals (LCP, INP, CLS), bundle size, or hydration cost
- Auditing or improving accessibility (contrast, keyboard, screen reader, focus, dynamic type)
- Auditing or improving on-page SEO (titles, descriptions, headings, structured data, canonicals)
- Reviewing visual polish, hierarchy, contrast, density, dark/light mode parity
- Replacing generic AI-flavored styling with a distinctive aesthetic direction
- Auditing or polishing an existing multi-page site against a reference design or production baseline
Skip this skill for backend logic, infrastructure, data pipelines, or non-visual scripting.
Loading policy (read before pulling references)
Each file under references/ averages 400 to 600 lines. Loading more than three at once burns context for content you do not need. Pick the 2 to 3 files that match the current task by reading the Routing tables below (By Task, By Symptom, then Reference Index). If a table does not resolve cleanly, read the YAML frontmatter at the top of candidate files (about 15 lines each, via head -20) before loading the rest. Drop a reference when the current task no longer matches its purpose.
North Star Targets
Every interface should meet these bars before being considered "done". Treat any failure as a blocking defect, not a polish task.
| Category | Mobile target | Desktop target | Source |
|---|---|---|---|
| Lighthouse Performance | >= 95 | >= 99 | Lighthouse |
| Lighthouse Accessibility | 100 | 100 | Lighthouse + axe |
| Lighthouse Best Practices | 100 | 100 | Lighthouse |
| Lighthouse SEO | 100 | 100 | Lighthouse |
| Largest Contentful Paint (LCP) | < 2.5s | < 2.0s | Core Web Vitals |
| Interaction to Next Paint (INP) | < 200ms | < 200ms | Core Web Vitals |
| Cumulative Layout Shift (CLS) | < 0.1 | < 0.05 | Core Web Vitals |
| Time to First Byte (TTFB) | < 800ms | < 600ms | Core Web Vitals |
| First Contentful Paint (FCP) | < 1.8s | < 1.0s | Lighthouse |
| Total Blocking Time (TBT) | < 200ms | < 100ms | Lighthouse |
| Initial JS payload (gzipped) | < 90 KB | < 130 KB | Budget |
| Initial CSS payload (gzipped) | < 25 KB | < 35 KB | Budget |
| Web font payload (per screen) | <= 2 families, <= 4 weights | same | Budget |
These are the universal bar. A given project may consciously relax a budget for a specific surface (e.g., a heavily interactive client-rendered tool that legitimately ships more JS), but the relaxation should be a recorded, justified exception, not a default.
Priority Stack (read top-down on any review)
- Crawlability and indexability (does the page exist in the index, with the right canonical and meta?)
- Accessibility correctness (semantic HTML, contrast, keyboard, focus, screen reader)
- Core Web Vitals (LCP, INP, CLS, TTFB)
- Layout and responsive integrity (no horizontal scroll, no clipped content, safe-area respect)
- Visual hierarchy and intentional aesthetic (typography, color, spacing, motion, density)
- Interaction quality (hover, press, focus, loading, error, empty, success states)
- Forms and feedback (validation, error placement, recovery, autofill, autosave)
- Motion and animation (purposeful, reduced-motion safe, transform-only)
- Data and charts (legible, accessible, responsive)
- Polish and craft (icon discipline, dark/light parity, brand fidelity)
Higher-priority items always block lower-priority items. Never trade contrast for aesthetics. Never trade INP for animation.
When rules conflict (tie-breakers)
The Priority Stack covers the common case. Use this table when two specific rules push the other way:
| Conflict | Tie-breaker |
|---|---|
| Animation polish vs INP budget | INP wins. An animation that drives INP over 200ms is a defect, not a polish item. |
| Aesthetic preference vs WCAG contrast | Contrast wins. No 3:1 large-text or 4.5:1 body floor is negotiable for visual taste. |
| LCP preload vs initial JS budget | JS budget wins. Find a smaller LCP element or defer the preload; do not blow the budget. |
| SSR for SEO vs INP under hydration cost | Split: SSR the shell for indexability, defer islands to keep INP under 200ms. |
Motion polish vs prefers-reduced-motion |
Reduced motion wins. Motion is the enhancement, not the baseline. |
| Brand color vs dark-mode text contrast | Use a lightened accent for text on dark surfaces; brand stays for backgrounds. See design.md. |
Decision: Render Strategy
A page lands in one of five render strategies. Choose by these axes; deep treatment in performance.md and seo.md.
| If ... | Pick |
|---|---|
| Indexable, mostly static content, low personalisation, traffic spike resilient | SSG (static site generation) |
| Indexable, per-request personalisation, dynamic data, server has compute budget | SSR (server-side rendering), optionally streamed |
| Index not required, deeply interactive, app-shell experience, INP-sensitive | CSR (client-side rendering), with route-level code splitting |
| Indexable, partial personalisation, want SSG for the shell with revalidation | ISR (incremental static regeneration) |
| Mostly static + a few interactive widgets, want minimal JS | Static plus islands (partial hydration / resumability) |
Decision rule of thumb: indexability first (SSG / SSR / ISR for indexable surfaces); interactivity cost second (split into islands if SSR pages run over the JS budget); fall back to CSR only when index is not required and the app shell is the product.
Routing: by task
Pick the row that matches what you have been asked to do. Load the named files first; pull others only if those files point you elsewhere.
| You are asked to ... | Load these files |
|---|---|
| Build a new page from scratch | components.md, design.md, responsive.md, performance.md |
| Audit an existing site against a reference | audit-workflow.md, defects.md, design.md |
| Fix a failing Lighthouse audit | lighthouse.md, performance.md, debug-recipes.md |
| Polish visual quality on an existing page | design.md, ui-ux.md, defects.md |
| Add a form or sign-in flow | forms.md, auth.md, accessibility.md |
| Set up CI gates and pre-merge checks | testing.md, lighthouse.md, observability.md |
| Diagnose a production issue | observability.md, debug-recipes.md, defects.md |
| Verify a page is done before shipping | pre-launch.md, quick-reference.md |
| Internationalise or localise a surface | i18n.md, responsive.md, accessibility.md |
| Harden security (CSP, headers, Trusted Types, SRI) | security.md, lighthouse.md, pre-launch.md |
| Add PWA, offline, or installability | pwa-offline.md, performance.md |
| Embed third-party widgets or be embedded | embed-patterns.md, security.md, auth.md |
| Print stylesheet or transactional email HTML | print-email.md |
| Add charts, tables, or data display | data-viz.md, ui-ux.md, accessibility.md |
| Add or refine motion and animation | motion.md, performance.md |
| Review a change against known anti-patterns | anti-patterns.md, defects.md |
Routing: by symptom
Use observable problem signals to jump straight to the relevant file and section.
| Symptom | Likely root, file and section to load |
|---|---|
| LCP > 2.5s, LCP regression | preload / fetchpriority / hydration cost: performance.md: Core Web Vitals |
| INP > 200ms, slow interaction | long task / handler / hydration: performance.md: Core Web Vitals, debug-recipes.md: INP regression |
| CLS > 0.1, layout jump | async element not reserved: performance.md: Core Web Vitals, defects.md: Defect Lookup Table |
| font swap CLS | missing size-adjust / metrics-override on the fallback: debug-recipes.md: Font-swap CLS, performance.md: Font Strategy |
| Lighthouse SEO < 100 | noindex on a graded page, missing canonical: lighthouse.md: Failing Audit to Fix Map, seo.md: Indexability |
| Lighthouse Best Practices < 100 | CSP violation, console errors, deprecation: lighthouse.md: Failing Audit to Fix Map, security.md: CSP at Depth |
| errors-in-console, stale SRI beacon | third-party beacon CORS / stale pinned hash, or a client-blocker false positive: debug-recipes.md: Lighthouse flake triage |
| phantom dev failure (low score only on the dev server) | scoring the dev server, not a production build: lighthouse.md: Run Lighthouse Properly |
| image too small on retina (image-size-responsive) | largest srcset candidate below CSS width times DPR: responsive.md: Responsive Images, lighthouse.md: Failing Audit to Fix Map |
| contrast fail, especially after a build change | low ratio, or a CSS-delivery change that reordered the cascade: accessibility.md: Color and Contrast, lighthouse.md: Failing Audit to Fix Map |
| duplicate id | reused component with a hardcoded id: components.md: Drift Detection |
| scroll lock side shift | modal missing scrollbar-width compensation: ui-ux.md: Modals and Overlays, defects.md: Defect Lookup Table |
| hydration mismatch | server vs client divergence: debug-recipes.md: Hydration mismatch, performance.md: JavaScript Strategy |
| viewport overflow, horizontal scroll | bleed in a child element: responsive.md: Horizontal Scroll: Forbidden, defects.md: Defect Lookup Table |
| focus trap leak, focus not visible | overlay focus management: debug-recipes.md: Focus trap leak, accessibility.md: Keyboard |
| third-party script slow, analytics blocking | third-party-script discipline / async-defer matrix: performance.md: Third-Party Strategy, build-hygiene.md: Dependency-Cost Discipline |
| broken on Firefox / Safari only | feature-support assumption, missing fallback: responsive.md: Media Preferences, motion.md: Reduced Motion |
| consent banner CLS, bounding box jumps | overlay not reserved: ui-ux.md: Modals and Overlays, seo.md: Indexability |
| aria-hidden background still announced, inert leak | overlay applied to the wrong ancestor: accessibility.md: Modal and Dialog, ui-ux.md: Modals and Overlays |
| hero animation regresses LCP or TBT | above-the-fold motion not gated against a budget: motion.md: Hero Animations |
Reference Index
Detailed guidance lives in references/. Each row gives a one-line purpose plus the keywords that should pull the file.
| File | Use when (keywords) |
|---|---|
| lighthouse.md | Lighthouse, audit, score, CI gate, csp-xss, duplicate-id, is-crawlable, score variance, user-flow |
| performance.md | LCP, INP, CLS, TTFB, performance, hydration, bundle, BFCache, Speculation Rules, Early Hints, fetchpriority, render strategy, SSR, SSG, CSR, third-party script, list virtualization |
| accessibility.md | a11y, WCAG, ARIA, screen reader, keyboard, focus, contrast, forced colors, reduced motion, dynamic type |
| seo.md | SEO, canonical, sitemap, robots, structured data, JSON-LD, AEO, GEO, llms.txt, hreflang, Open Graph |
| ui-ux.md | UI, UX, modal, popover, dialog, drawer, menu, tooltip, breadcrumb, touch target, hit target, popover API, inert |
| design.md | design, typography, color, OKLCH, P3, dark mode, brand, variable font, @layer |
| responsive.md | responsive, breakpoint, container query, viewport, safe area, dvh, srcset, subgrid, scrollbar-gutter |
| motion.md | motion, animation, transition, easing, View Transitions, scroll-driven, WAAPI, @starting-style |
| forms.md | form, validation, input, autofill, autocomplete, constraintValidation |
| data-viz.md | chart, data viz, axis, legend, Canvas, SVG, WebGL, timezone |
| pre-launch.md | pre-launch, checklist, ship, gate, verification, evidence |
| audit-workflow.md | audit, route, sweep, baseline, capture, polish, multi-page |
| components.md | component, contract, extraction, slots, composition, tokens, Storybook |
| defects.md | defect, geometry, sweep, threshold, regression, lookup |
| security.md | CSP, COOP, COEP, CORP, cross-origin isolation, Trusted Types, SRI, Permissions-Policy, Referrer-Policy, supply chain |
| observability.md | RUM, observability, monitoring, source maps, error capture, INP attribution, Reporting API, CrUX, LoAF |
| testing.md | testing, visual regression, axe-core, pa11y, size-limit, bundlesize, lighthouse-ci, contract test |
| auth.md | auth, login, passkey, WebAuthn, OAuth, magic link, session, account recovery, CAPTCHA, Storage Access API |
| debug-recipes.md | debug, recipe, hydration mismatch, layout overflow, focus trap, font-swap CLS, Lighthouse-flake |
| anti-patterns.md | anti-pattern, what to avoid, mistake |
| i18n.md | i18n, l10n, locale, translation, Intl, plural rules, bidi, RTL, mirroring, hreflang |
| pwa-offline.md | PWA, offline, service worker, install prompt, push, background sync, manifest |
| build-hygiene.md | build, tree-shaking, sideEffects, lockfile, dead code, code splitting, dependency cost |
| embed-patterns.md | embed, iframe, sandbox, postMessage, host, guest, third-party widget |
| print-email.md | print, email, @page, page-break, transactional email, Outlook |
| quick-reference.md | rule, quick reference, highest leverage |
Workflow
1. Frame the work
Before writing code, capture: audience and intent; indexability (search-indexed surface vs auth wall); aesthetic direction (one bold lane; refuse "Inter on white with a purple gradient"); constraints (render strategy from the table above, hydration model, browser support floor, locale, writing direction).
The rendered browser is the source of truth at every phase. Do not judge visual quality from code alone; capture screenshots, compare against the reference, fix, and re-capture. When the work is auditing or polishing an existing multi-page site rather than building new, switch to the Multi-Page Polish Loop below.
2. Plan the structure
One H1 per page; sequential headings. Land the LCP element in the first viewport (a real, served asset with declared dimensions, not lazy-loaded, not behind hydration). Reserve space for every async element (aspect-ratio, fixed height, skeleton). Decide which sections are server-rendered, which are streamed, which are deferred, which are client-only; justify every client-only choice.
3. Build with budgets
Declare and verify per surface: JS payload (initial route, gzipped); CSS payload (initial route, gzipped); image payload (above the fold); font budget (families, weights, subsets, byte total); third-party script budget (count, total ms of main-thread time). Reject any addition that breaks a budget without an explicit, recorded waiver.
4. Verify
Run the full Lighthouse + axe + visual + responsive + dark-mode + reduced-motion + keyboard + screen-reader pass before declaring the work complete. See pre-launch.md.
Multi-Page Polish Loop
When the work is auditing or polishing an existing multi-page site, read audit-workflow.md first for the full procedure (operating rules, capture script, per-phase detail, final acceptance gate). The 19 phases index:
- Discover context. 2. Inventory routes. 3. Inventory repeated widgets. 4. Capture baseline. 5. Capture reference. 6. Audit each route. 7. Prioritise fixes. 8. Patch with component discipline. 9. Patch with design discipline. 10. Verify after every fix group. 11. Programmatic geometry checks (and content-and-markup sweep). 12. Component drift checks. 13. Interaction QA. 14. Accessibility validation. 15. Lighthouse and performance polish. 16. Reference-level design heuristics. 17. Common defects and fixes. 18. Deliverables. 19. Final acceptance gate.
Component extraction discipline lives in components.md; defect lookup and the geometry sweep in defects.md.
Quick Reference
The highest-leverage rules across performance, accessibility, SEO, UI/UX, design, and multi-page consistency live in references/quick-reference.md. Open that file when you want a single condensed view to scan against a change.
Skill freshness
This skill encodes web-platform behaviour at a point in time. Before applying an old rule, double-check it is still current when:
- The rule cites a Lighthouse audit ID, weight, or category cutoff (Lighthouse rebalances categories every few releases).
- The rule cites a browser-support floor (Baseline status moves; check caniuse.com or the MDN Baseline widget).
- The rule cites a specific Core Web Vitals threshold or metric name (INP replaced FID in 2024; thresholds are revisited).
- The rule names a CSS or JS API ([
@layer, View Transitions, Anchor Positioning, Popover,:has()] all changed support between 2023 and 2026).
See CHANGELOG.md for the dated history of additions and refinements.
Self-Improvement
After applying this skill, before declaring work complete:
- Did Lighthouse run on real production builds (not dev mode) with throttled mobile and desktop presets?
- Did axe (or equivalent) run with zero violations on every changed page?
- Did the page render correctly at 320px, 375px, 768px, 1024px, 1280px, and 1920px?
- Did the page render correctly in light and dark mode independently (not by CSS-inverting one)?
- Did the page render correctly with
prefers-reduced-motion: reduce? - Did the page work end-to-end with keyboard only?
- Did the page survive a screen reader pass (VoiceOver or NVDA) for the primary task?
- Did the LCP element ship with
fetchpriority="high", declared dimensions, and a real served asset? - Did all custom fonts load with
font-displayset, and is the count <= 2 families / 4 weights? - Did every async section reserve space?
- Did every interactive element have visible focus and meet contrast?
- Did the page expose unique title, description, canonical, OG, Twitter, and (where applicable) JSON-LD?
- Did you verify the SHIPPED build output (copied headers, static files, generated images), not just the source config?
For multi-page audit and polish work, also confirm:
- Was a widget inventory built for every shared family (header, hero, section, CTA, card, media, list, interactive, empty, legal, 404)?
- Were baseline screenshots captured at
1440x900and375x812before any edits? - Were after screenshots captured at
1440x900and375x812for every changed route? - Did the geometry sweep return zero issues on every audited route at both viewports?
- Did you run BOTH programmatic sweeps (geometry, and content-and-markup) on every route?
- Did the drift collector return no unexplained differences for any shared widget family?
- Was a per-route audit table (issues found, fixes applied, before, after) delivered with the change?
If any answer is "no" or "not checked", the work is not done.