Stack-agnostic frontend patterns. Use when writing or modifying any UI code (React, React Native, Vue, Svelte) regardless of framework. Covers three-state async UI, loading/error/empty handling, client vs server components, hydration safety, accessibility, performance, SEO basics, and state management patterns.
Scanned 9/4/2026
Install to Claude Code
npx -y skills add kouroshez/coding-os --skill frontend-fundamentals --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Frontend Fundamentals?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kouroshez-frontend-fundamentals-cbb521e2)More formats (shields.io, HTML) on the badges page.
---
name: frontend-fundamentals
tier: layer
domain: [frontend, mobile]
description: Stack-agnostic frontend patterns. Use when writing or modifying any UI code (React, React Native, Vue, Svelte) regardless of framework. Covers three-state async UI, loading/error/empty handling, client vs server components, hydration safety, accessibility, performance, SEO basics, and state management patterns.
globs: "frontend/**/*"
paths: ["frontend/**/*"]
context: fork
depends_on:
- clean-code
allowed-tools:
- Read
- Grep
- Glob
- Edit
- Write
last_reviewed: "2026-05-11"
---
# Frontend Fundamentals — Stack-Agnostic UI Patterns
Universal guidance for every component-based UI framework: React, React Native, Vue, Svelte, Solid. Framework-specific layering (Next.js App Router, React Native navigation, Vue composition API) lives in the stack-specific skill that `depends_on: [frontend-fundamentals]`.
Loaded automatically when `enforce-skill.sh` routes a file under `frontend/` that matches a stack skill.
## 0. Every async UI is three-state
Every screen or section that fetches data MUST explicitly handle three states. Missing any of them is a bug:
| State | Required UI |
|---|---|
| **Loading** | skeleton or spinner; indication of progress |
| **Error** | actionable message + retry affordance; never a blank screen |
| **Empty** | explains why it's empty + suggests next action; never an ambiguous zero rows |
A fourth state — **stale/refreshing while data exists** — is a common polish item. Render the previous data with a subtle loading indicator; don't blank the UI.
Anti-pattern: `{data && <List data={data} />}`. This silently hides error AND empty AND loading behind a single falsy check.
## 1. Server components vs. client components (React 19+ / Next 15+)
Default everything to server components. Convert to client only when one of these is present:
- **Interactive state** (`useState`, `useReducer`, form state).
- **Event handlers** (`onClick`, `onChange`, `onSubmit`).
- **Browser-only APIs** (`window`, `localStorage`, `navigator`, `document`, `IntersectionObserver`).
- **Hooks that require a client** (`useEffect`, `useLayoutEffect`, most third-party hooks).
Server components don't ship JS to the browser and can fetch directly. This is the single biggest first-load perf win in modern React. A component that's `"use client"` without a reason is accidental bundle bloat.
## 2. Hydration safety
Anything whose output differs between the first server render and the first client render causes a hydration mismatch. Common culprits:
- `new Date()`, `Math.random()` at top level of a client component.
- `window.matchMedia(...)` defaulting to different value.
- Reading `localStorage` synchronously during render.
Fix: move non-deterministic values to `useEffect` + `useState`, render the deterministic initial value during SSR, swap after mount. For `localStorage`-backed state, guard with `typeof window !== "undefined"` AND initialize via effect to avoid SSR/CSR drift.
## 3. Loading / error / empty component contract
Build a shared tri-state component once per project:
```tsx
<AsyncBoundary
isLoading={query.isLoading}
error={query.error}
isEmpty={query.data?.length === 0}
onRetry={query.refetch}
emptyCTA={<CreateFirstItemButton />}
>
<List items={query.data} />
</AsyncBoundary>
```
Forces callers to think about all three states. A `null` / `undefined` passed for `isEmpty` is a linter error, not a silent skip.
## 4. Error boundaries — at every major route
- One error boundary per route segment. Never a single root boundary (a crashed card shouldn't white-screen the whole app).
- Error boundary UI: explain what happened + "Try again" button + "Contact support" link. Never just "Something went wrong."
- Log the error to the telemetry backend (Sentry, PostHog, custom endpoint) with the component tree context.
- Error boundaries DON'T catch: async errors inside `useEffect`, event handler errors, errors in server components on a separate request. Handle those with try/catch + explicit state.
## 5. Accessibility — the minimum floor
- **Every interactive thing has a label.** Buttons have text or `aria-label`; form inputs have `<label htmlFor>`; icons that act as buttons have `aria-label`.
- **Keyboard navigable.** Tab reaches every focusable element, Enter/Space activates, Escape closes. Custom dropdowns/modals/menus must trap focus when open and restore on close.
- **Visible focus ring.** Never `outline: none` without replacing it. Focus ring is legally required in many jurisdictions.
- **Semantic HTML first.** `<button>` not `<div onClick>`. `<nav>`, `<main>`, `<article>`, `<section>` not `<div>` soup. Heading order (h1→h2→h3) doesn't skip.
- **Alt text on every `<img>`.** Decorative → `alt=""`. Informative → describe the content.
- **Color contrast ≥ 4.5:1** for normal text (AA). Test with browser DevTools contrast checker; don't eyeball.
Use `axe-core` / `eslint-plugin-jsx-a11y` in CI. Manual keyboard-only walkthrough of the happy path before every release.
## 6. Performance — the non-negotiables
- **Lazy-load route-level code splits.** Every route is a dynamic import boundary.
- **Lazy-load below-the-fold components** (modals, carousels, rare tabs).
- **Images: width + height always, `loading="lazy"` for below-fold, responsive `srcset` for above-fold hero.**
- **Memoize expensive derivations**, NOT every `useCallback`. Memoization has a cost — use it when the dependency array is stable AND the downstream comparison is expensive.
- **Virtualize lists > 100 items** (`react-window`, `tanstack/virtual`). Rendering 1000 DOM nodes makes scroll jank on mobile.
- **Target Core Web Vitals**: LCP < 2.5 s, INP < 200 ms, CLS < 0.1. Measure on real devices, not local Chrome on fiber.
## 7. State management — smallest scope that works
Default to component-local `useState`. Promote up the tree ONLY when:
- Two siblings need the same state → lift to closest common parent.
- Many distant components need it → global store (Zustand / Jotai / Redux Toolkit / Pinia).
- Server data → use a data-fetching library (TanStack Query / SWR / RTK Query). **Never** store server data in a client store manually; it invites stale-data bugs.
Separate three kinds of state:
1. **Server state** (cached from backend) — TanStack Query / SWR.
2. **URL state** (what's selected, active tab, filters) — query params (survives refresh + shareable).
3. **Client-only UI state** (dropdown open, hovered item) — local `useState`.
Mixing these in one store is the origin of 80 % of "why is the UI wrong after a refresh" bugs.
## 8. Form handling
- **Controlled inputs** for anything with validation or dependent logic.
- **Validate client + server.** Client for UX (fast feedback), server as the authority. The server's validation schema IS the contract.
- **Disable submit while submitting.** Re-clicking a submit button during the request is one of the most common double-submit bugs.
- **Show field-level errors inline** next to the input, not a single toast at the top.
- **Announce errors to screen readers** (`aria-live="polite"`).
Libraries: `react-hook-form` + `zod` / `yup`; `formik`; `vee-validate` for Vue. All handle the boring parts (dirty/touched, field-level errors) correctly.
## 9. SEO basics (web, not mobile)
For every page that should be indexable:
- **Unique `<title>`** — includes page name + brand, < 60 chars.
- **`<meta name="description">`** — unique per page, 140–160 chars.
- **Open Graph** (`og:title`, `og:description`, `og:image`, `og:url`) — the social-share preview.
- **Twitter Card** — `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`.
- **Canonical URL** — `<link rel="canonical">` pointing at the locale-specific version. Do NOT share canonicals across locales.
- **Structured data (JSON-LD)** for product / article / breadcrumb / FAQ pages — `<script type="application/ld+json">`.
- **Semantic HTML** helps crawlers (see §5).
## 10. i18n — if the app ships in > 1 language
- **One string catalog per locale** (`en.json`, `fa.json`, ...). No hardcoded UI strings in components.
- **Pluralization** via ICU MessageFormat or framework built-in (`Intl.PluralRules`).
- **RTL support** for Arabic / Hebrew / Persian — use logical CSS properties (`margin-inline-start` not `margin-left`).
- **Locale-aware formatting** for dates, numbers, currencies (`Intl.DateTimeFormat`, `Intl.NumberFormat`).
- **URL structure** — `/` default + `/<locale>/` for additional locales. Ship `<html lang>` and `<link rel="alternate" hreflang>`.
## 11. localStorage / sessionStorage — SSR-safe
```tsx
// WRONG — crashes on SSR and hydration
const [theme, setTheme] = useState(localStorage.getItem("theme") ?? "light");
// RIGHT — deterministic initial render, swap after mount
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const stored = localStorage.getItem("theme");
if (stored === "light" || stored === "dark") setTheme(stored);
}, []);
```
Never access `window` / `localStorage` / `document` at module top level or during render of a component that might SSR. Pattern: initialize with a deterministic default, swap inside an effect.
## 12. Mobile-first (React Native + responsive web)
- **Design for touch.** Min tap target 44×44 px (iOS HIG) / 48 dp (Material).
- **Respect safe areas.** Use `SafeAreaView` (React Native) / `env(safe-area-inset-top)` (web). Content under the notch is a bug.
- **Offline-first** for mobile networks. Cache read-heavy data, queue writes, reconcile on reconnect.
- **Optimistic UI** for write actions when a rollback path is cheap. Show the action as done immediately; reconcile if the server rejects.
## 13. Testing — the minimum coverage
- **Unit test components with behavior.** Render + assert interaction outcomes (`testing-library/react`, `@testing-library/react-native`). Test from the user's perspective, not implementation (role / text, not class names).
- **Three-state test per async component.** Render while loading, resolved with data, resolved empty, rejected — each is a separate test.
- **E2E happy path** on the 3–5 critical flows (login, checkout, main CRUD). Playwright / Cypress / Detox.
- **Snapshot tests sparingly.** They're easy to write and hard to trust; prefer explicit assertions.
## 14. Pre-commit / PR checklist
Before opening a PR touching a frontend file:
- [ ] Server component unless something forces `"use client"`
- [ ] Loading / error / empty states all explicitly rendered
- [ ] Error boundary at route level
- [ ] Keyboard-only nav works end-to-end (tab + enter + escape)
- [ ] Focus-visible ring present on all interactive elements
- [ ] Images have `width`/`height` + `alt`
- [ ] Form validates client-side AND server-side
- [ ] No top-level `localStorage` / `window` access in components that SSR
- [ ] `<title>` + `<meta description>` + canonical per indexable page
- [ ] Core Web Vitals not regressed (LCP / INP / CLS checked in DevTools)
## References
Stack-specific specializations extend this skill via `depends_on: [frontend-fundamentals]` — your project's framework skill (nextjs-react / react-native-mobile) is present when that stack is installed.
## Tooling
Flag React re-render + key smells (inline-ref props, index keys, dangerous HTML):
`python3 scripts/check_frontend.py src/frontend/**/*.tsx`
## See also
- [references/rendering-and-state.md](references/rendering-and-state.md) — re-renders, keys, state location, data fetching.
- [assets/frontend-checklist.md](assets/frontend-checklist.md) — the review gate.
- [frontend-design](../frontend-design/SKILL.md) — visual/UX design principles (any framework) · [state-management](../state-management/SKILL.md) · [a11y](../a11y/SKILL.md) · [performance](../performance/SKILL.md).
Universal principles from [clean-code](../clean-code/SKILL.md) still apply — this skill does not relax any of them.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!