# Building Frontends

> The foundational guide for how Claude should build frontend systems — the rules, defaults, and judgment that apply to every UI before reaching for a specific playbook. Read this FIRST for any frontend work (new screen, component, form, data fetching, styling). Covers component architecture, state, data fetching, forms, accessibility, performance, styling, and testing, and points to the specific skills (react-component-builder, frontend-state-architecture, web-performance-audit) for the deep dives.

- Skill: `omonuj/building-frontends` (Agent Skill)
- Install (CLI): `npx skillmds@latest add omonuj/building-frontends`
- Raw SKILL.md: https://api.skillmd.com/api/skills/omonuj/building-frontends/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: omonuj (https://skillmd.com/u/omonuj)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/omonuj/building-frontends

---


# building-frontends

This is the backbone skill for frontend work. Read it before writing any UI code — it sets the defaults and non-negotiables so every screen comes out consistent, accessible, and maintainable, then hands off to the specific skills for the detailed procedures. If a rule here conflicts with a local repo convention, the repo wins; otherwise, follow it.

## First, understand before you build
- **Read the existing UI before adding to it.** Match the component library, styling approach, state tooling, routing, and data-fetching patterns already in use. A new screen that invents its own button, its own modal, its own fetch pattern is technical debt on arrival.
- **Build from the design system, don't reinvent it.** Reach for the existing primitives (Button, Input, Modal) before writing a new one. If a primitive is missing, add it *to the system*, not inline in one screen.
- **Think in states, not screens.** Every view has more than a happy path: loading, empty, error, partial, and the permission-denied variant. Design all of them up front — a UI that only handles "has data" is half-built.

## Component architecture — the rules
- **Separate container from presentation.** Presentational components take props and emit callbacks and hold no business logic or data fetching; containers wire data and behavior to them. This keeps the reusable pieces dumb and testable.
- **Compose small components; lift state only as far as needed.** State lives at the lowest common ancestor of the components that use it — no higher. A giant top-level component holding everyone's state is a re-render bomb and a merge-conflict magnet.
- **Props are a contract: type them honestly.** Extend the underlying element's props so you inherit its behavior; use discriminated unions for mutually-exclusive shapes; no `any`. A component's prop types are its documentation.
- **Derive, don't duplicate.** If a value can be computed from props/state during render, compute it — don't store a copy and sync it with an effect. The `useEffect`-to-mirror-a-prop pattern is a bug source (flicker, stale values); it is almost never correct.
- **Effects are for synchronizing with the outside world** (subscriptions, imperative DOM, non-React widgets), not for reacting to your own state. If an effect only reads and writes React state, the logic belongs in an event handler or a derived value.

→ For building a single reusable component to standard (controlled/uncontrolled, a11y, all states, forwarded refs), use **react-component-builder**.

## State — put each piece where it belongs
The most common frontend mess is state in the wrong place. Classify every piece and place it once:
- **Server data → a data-fetching cache** (React Query/SWR/RTK Query). It's a cache of someone else's data, not your state. Never copy it into `useState` — you'd own cache invalidation and get it wrong.
- **Shareable/refresh-surviving UI (filters, tab, sort, selected id) → the URL.** If it should survive a refresh or be linkable or work with the back button, it belongs in query params, not component state.
- **Form input → a form library / local state** until submit.
- **Local UI (modal open, hover, wizard step) → local `useState`,** lifted only as needed.
- **Genuinely app-wide client state (auth, theme, flags) → a small global store,** selected narrowly. Not a dumping ground, and never server data.

→ For the full decision tree, cache rules, and normalization, use **frontend-state-architecture**.

## Data fetching — the rules
- **Use the cache library; don't hand-roll fetch-in-`useEffect`.** Manual fetching leaks race conditions (out-of-order responses), lacks dedup/caching, and reinvents the library badly. If you truly must, cancel stale requests with an `AbortController` and ignore late responses.
- **Every async boundary renders loading, error, and empty** — not just success. A spinner that never resolves on error is a broken screen.
- **Mutations invalidate the cache** (or optimistically update *with a rollback path*). An optimistic update with no rollback on error is a bug.
- **Handle the network reality:** retries, stale-while-revalidate where appropriate, and a visible, recoverable error state (a "retry" affordance, not a dead end).

## Forms — the rules
- **Controlled where you need validation/derived UI; uncontrolled for simple cases.** Pick one per form and be consistent.
- **Validate on the right trigger:** on blur / on submit, not aggressively on every keystroke (which fights the user as they type). Show errors after the field is touched, not before.
- **Field-level, specific error messages** tied to inputs via `aria-describedby`; mark invalid fields with `aria-invalid`. "Something went wrong" is not a validation message.
- **Disable submit while submitting; make submission idempotent.** Prevent the double-submit; show progress; on failure, keep the user's input and show what failed.
- **Never trust client validation for security** — it's a UX affordance; the server validates authoritatively.

## Accessibility is a requirement, not a feature
- **Semantic HTML first, ARIA second.** A real `<button>`/`<a>`/`<label>`/`<nav>` before a `<div>` with handlers. ARIA only fills the gaps semantic elements can't.
- **Keyboard-operable everything.** Every interactive element reachable and usable by keyboard, with a visible focus ring. Never `outline: none` without a replacement.
- **Manage focus** for overlays: move focus in on open, trap it while open, restore to the trigger on close.
- **Non-visual state:** `aria-expanded`, `aria-selected`, `aria-invalid`, `aria-busy` where relevant; accessible names on every control.
- **Don't rely on color alone;** meet WCAG AA contrast; respect `prefers-reduced-motion`.
- Test by querying the accessible tree (by role + name), which both verifies behavior and enforces a11y.

## Performance — measure, don't guess
- **Ship less JavaScript.** Code-split by route; lazy-load heavy, below-the-fold, or interaction-triggered components (modals, editors, charts). The fastest script is the one you don't send.
- **Don't optimize on a hunch.** No blanket `useMemo`/`useCallback`/`memo` — each has a cost and most components are cheap. Add memoization only where a profiler shows an expensive render firing too often. Virtualize genuinely long lists.
- **Guard the vitals:** reserve space for images/async content (no layout shift), prioritize the LCP image, keep the main thread free for interaction. Set a bundle budget in CI so wins don't regress.

→ For diagnosing and fixing Core Web Vitals against measurements, use **web-performance-audit**.

## Styling
- **Follow the repo's system** (Tailwind, CSS Modules, CSS-in-JS) — don't add a second styling paradigm.
- **Use design tokens** (spacing, color, type scale) rather than magic numbers; theme via tokens so light/dark and rebrands are a config change, not a find-replace.
- **Responsive and fluid by default;** relative units, flexbox/grid; wide content (tables, code) scrolls in its own container so the page never scrolls horizontally.

## Testing stance
- **Query the way a user (and assistive tech) does** — by role, label, and text — via Testing Library. Avoid test ids and class selectors unless nothing else is stable.
- **Test behavior and the non-happy states,** not implementation details (no asserting on internal state or specific hook calls) — so tests survive refactors.
- **Include an a11y assertion** (axe) where tooling allows; add a keyboard-interaction test for custom widgets.
- **Every bug fix gets a regression test.**

## Procedure for any frontend task
1. Read the surrounding UI; reuse the design system and existing patterns.
2. Enumerate every state (loading/empty/error/partial/denied/success) before building.
3. Classify state and place each piece (server-cache / URL / form / local / global).
4. Fetch via the cache library with loading+error+empty; mutations invalidate or roll back.
5. Build components container/presentation split, typed honestly, deriving not duplicating.
6. Run the a11y checklist (semantic HTML, keyboard, focus, non-visual state, contrast).
7. Keep it fast by construction (code-split, no speculative memo); guard vitals + bundle budget.
8. Test by role/behavior, cover the non-happy states, add regression tests; confirm against the specific skill's `Definition of done`.

## Definition of done
- Reuses the design system and repo conventions; no duplicated primitives or styling paradigms.
- All states implemented (loading/empty/error/partial/denied), not just the happy path.
- State placed correctly: server data in the cache, shareable UI in the URL, no `useState` mirrors of server data.
- Data fetching via the cache lib with loading/error/empty and rollback on optimistic writes.
- Accessible: semantic HTML, keyboard-operable, focus managed, non-visual state, AA contrast.
- Fast by construction: code-split, no speculative memoization, vitals guarded, bundle within budget.
- Tests query by role/behavior, cover non-happy states, include a11y + regression coverage.

