SKILL.md
Frontend UI Engineering
Ship UI that looks like a design-aware engineer built it: real design system adherence, WCAG 2.1 AA accessibility, all four states handled (loading, error, empty, success). The recognizable "AI aesthetic" is a quality signal, and the signal says low.
Workflow
- Learn the project's system first. Find the spacing scale, color tokens, radius scale, and type hierarchy before writing a component. No design system: use a 0.25rem spacing scale, semantic tokens, and restraint.
- Structure the component. Colocate component, test, hook, and types in one directory. Composition over configuration: children and slots beat a dozen props. Split anything past ~200 lines. Separate data fetching (container) from rendering (presentation). Read references/patterns.md when you need the code patterns for composition, container/presentation, focus management, or optimistic updates.
- Pick the simplest state tool that works. useState for local UI, lifted state for 2-3 siblings, context for read-heavy globals (theme, auth), URL params for shareable state (filters, pagination), React Query/SWR for server data, a global store only for complex app-wide client state. Prop drilling past 3 levels means restructure.
- Handle all four states. Every data-driven view renders loading (skeleton, not spinner), error (message plus retry), empty (guidance plus action), and success. A blank screen in any state is a bug.
- Build accessibility in. Native elements first (
<button>, not<div onClick>). Label every icon-only control and every input. Move focus on dialogs and route changes, trap it in modals. Contrast 4.5:1 normal text, 3:1 large. Never color as the only signal. - Make it responsive mobile-first. Base styles for 320px, expand with breakpoints. Test at 320, 768, 1024, 1440.
- Kill the AI aesthetic. Checklist of defaults to reject: purple/indigo palette, gradients everywhere, rounded-2xl on everything, generic hero sections, lorem ipsum copy, uniform oversized padding, stock card grids, layered shadows. Replace each with the project's actual system and content-first layout.
Good vs bad
Bad: A dashboard card grid: every card rounded-2xl shadow-xl p-8, indigo gradient header, spinner while loading, blank div when the list is empty, <div onClick> for the row actions.
Good: Cards use the project's radius and spacing tokens, the grid prioritizes the two metrics users scan first, loading renders a skeleton matching final layout, empty state says "No tasks yet" with a create button, rows are real buttons reachable by Tab.
Verification
Do this pass on the finished UI. Tab through the entire page: every interactive element receives visible focus and activates with Enter/Space. Run axe-core (or the browser a11y devtools) and resize to 320px. Expect: zero console errors, zero axe violations, no horizontal scroll at 320px, and all four states reachable. Any failure: fix it before handing off, "works with a mouse at 1440px" is not done.
Completion checklist
- [ ] Project design tokens used, no invented spacing or raw hex values
- [ ] Component colocated, under ~200 lines, composition-based
- [ ] State managed with the simplest sufficient tool
- [ ] Loading, error, empty, success all implemented
- [ ] Keyboard pass clean, labels on all unlabeled controls
- [ ] Contrast and non-color signals verified
- [ ] Works at 320, 768, 1024, 1440
- [ ] No AI-aesthetic defaults present
Any box unchecked: not done. Fix or say so.
Red flags
Verbatim rationalizations that precede low-quality UI:
- "Accessibility is a nice-to-have for now"
- "We'll make it responsive later"
- "This is just a prototype"
- "The design isn't final, so styling can wait"
- "The AI look is fine for a first pass"
Closure rule: prototypes become production. If the code will be seen by a user or a reviewer, the standards above apply to it now, not later.
Footguns
- Spinner-for-everything loading. Spinners cause layout shift and hide structure. Fix: skeletons that match the final layout,
aria-busyon the container. - Div soup interactivity.
<div onClick>is invisible to keyboards and screen readers. Fix: native<button>/<a>; if forced, add role, tabIndex, and both Enter and Space handling. - Arbitrary values.
padding: 13pxand#7c3aedscattered around defeat the system. Fix: tokens and scale values only; an arbitrary value is a signal you skipped step 1. - Optimistic updates without rollback. UI updates, request fails, state lies. Fix: snapshot previous state in onMutate, restore it in onError (pattern in references/patterns.md).