Awesome-omni-skill developing-frontend-apps
Frontend application development best practices. Use when building, modifying, or reviewing frontend applications, React components, UI components, client-side JavaScript/TypeScript, CSS/styling, single-page applications, or web application architecture.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/developing-frontend-apps" ~/.claude/skills/diegosouzapw-awesome-omni-skill-developing-frontend-apps && rm -rf "$T"
skills/development/developing-frontend-apps/SKILL.mdFrontend Application Development Best Practices
Component Architecture
- One component, one job. If it fetches data AND renders a complex UI, split it.
- Composition over inheritance — use children, render props, and custom hooks.
- Presentational components receive data via props and emit events. Container components handle data fetching and state.
- Co-locate related files — component, styles, tests, types in the same directory.
- Define an explicit Props interface for every component:
interface UserCardProps { user: User; onEdit: (id: string) => void; variant?: 'compact' | 'full'; } function UserCard({ user, onEdit, variant = 'full' }: UserCardProps) { // ... } - Avoid prop drilling beyond 2 levels — use context or composition (pass components, not data).
- Keep components under ~200 lines. Extract hooks when logic dominates the render.
State Management
- Local state first.
oruseState
for component-scoped concerns.useReducer - Lift state only when siblings share it. Move to the nearest common ancestor, no higher.
- Server state is not app state. Use a data-fetching library (TanStack Query, SWR) — it handles caching, refetching, optimistic updates.
- Minimal global state. Reserve for truly app-wide concerns: auth, theme, locale.
- Derive, don't duplicate. Compute values from source state:
// Bad: syncing filtered list into separate state const [items, setItems] = useState<Item[]>([]); const [filtered, setFiltered] = useState<Item[]>([]); // Good: derive from source const filtered = useMemo( () => items.filter(i => i.status === activeFilter), [items, activeFilter] ); - Global state libraries (when context isn't enough):
- Zustand — minimal API, great for simple global state (auth, UI toggles). No boilerplate.
- Jotai — atomic state, bottom-up approach. Good for independent pieces of state that compose.
- Redux Toolkit — full-featured, middleware, devtools. Use for complex state with many interdependent slices.
- Pick Zustand by default. Reach for Redux Toolkit only when you need middleware, time-travel debugging, or complex normalized state.
- Forms: use React Hook Form or TanStack Form for multi-field forms with validation. Manual
per field doesn't scale past 3-4 fields — validation, dirty tracking, and error display become unwieldy.useState - Immutable updates. Spread or
— never mutate state directly.structuredClone - URL as state. Search params, filters, pagination belong in the URL for shareability and back-button support.
Performance
Bundle size
- Tree-shake — use ES modules, avoid barrel files that pull entire libraries.
- Code-split at route boundaries with
. Lazy-load heavy components (editors, charts, maps).lazy() - Run
ornpx vite-bundle-visualizer
to find bloat.source-map-explorer - Target: initial JS payload under 200KB gzipped.
Core Web Vitals
- LCP — preload hero image, inline critical CSS, avoid render-blocking scripts.
- INP — keep main thread free, defer non-critical work, use
for expensive updates.startTransition - CLS — set explicit dimensions on images/video, reserve space for dynamic content, avoid layout shifts from web fonts.
Rendering
- Virtualize long lists (TanStack Virtual, react-window) — never render 1000+ DOM nodes.
- Memoize expensive components with
and stable callback references withReact.memo
. Profile first — premature memoization adds complexity without measurable gain.useCallback - Lazy-load images with
and always setloading="lazy"
/width
attributes.height
Accessibility
- Semantic HTML. Use
,<nav>
,<main>
,<aside>
,<section>
,<header>
. Native interactive elements over styled divs.<footer> - Keyboard operable. Every interactive element reachable via Tab. Custom widgets need arrow key navigation.
- Alt text. Descriptive for informational images. Empty
for decorative images.alt="" - Form labels. Every input needs a
. Link error messages with<label>
:aria-describedby<label htmlFor="email">Email</label> <input id="email" aria-describedby="email-error" aria-invalid={!!error} /> {error && <span id="email-error" role="alert">{error}</span>} - ARIA only when HTML falls short. A
already has<button>
— don't add it again.role="button" - Color contrast. 4.5:1 for normal text, 3:1 for large text (18px+ bold or 24px+).
- Focus management. Trap focus in modals. Restore focus to the trigger element on close.
- Test with a screen reader. VoiceOver (macOS), NVDA (Windows). Run
in CI.axe-core
CSS Architecture
- Scoped styles. CSS Modules (
) or Tailwind utility classes. Avoid global stylesheets beyond reset/tokens..module.css - Design tokens. Define colors, spacing, typography as CSS custom properties on
::root:root { --color-primary: oklch(55% 0.25 260); --space-sm: 0.5rem; --space-md: 1rem; --radius-md: 0.5rem; --font-body: system-ui, sans-serif; } - Mobile-first. Base styles for small screens,
for larger.@media (min-width: ...) - Logical properties.
,margin-inline
,padding-block
instead of directional properties — supports RTL layouts.inline-size - No magic numbers. Use tokens,
/em
, orrem
. Every value should have a reason.calc() - Prefer gap.
on flex/grid replaces margin hacks and adjacent sibling selectors.gap - Respect motion preferences. Wrap animations in
. Provide a static alternative for users with vestibular disorders.@media (prefers-reduced-motion: no-preference)@media (prefers-reduced-motion: no-preference) { .card { transition: transform 0.2s ease; } .card:hover { transform: scale(1.02); } } - Performant animations. Animate only
andtransform
— they run on the compositor thread, avoiding layout/paint. Useopacity
sparingly and remove after animation completes.will-change - CSS Nesting. Native nesting without preprocessors. Nest related selectors to co-locate styles:
.card { padding: var(--space-md); & .title { font-weight: 700; } &:hover { box-shadow: 0 2px 8px oklch(0% 0 0 / 0.1); } @media (min-width: 768px) { padding: var(--space-lg); } } - Container Queries. Size components based on their container, not the viewport — essential for reusable components:
.card-container { container-type: inline-size; } @container (min-width: 400px) { .card { grid-template-columns: 1fr 2fr; } } - Popover API. Declarative popovers without JavaScript — handles dismiss-on-outside-click, top-layer stacking, and focus management:
<button popovertarget="menu">Open</button> <div id="menu" popover>Popover content</div> - View Transitions API. Animated transitions between DOM states or pages with
. Pair withdocument.startViewTransition()
CSS property to animate specific elements between states.view-transition-name
TypeScript for Frontend
- Strict mode. Enable
instrict: true
. No exceptions.tsconfig.json - Props and state interfaces. Define them explicitly — never inline complex types:
interface SearchState { query: string; results: SearchResult[]; status: 'idle' | 'loading' | 'error' | 'success'; } - Avoid
. Useany
and narrow with type guards:unknownfunction isApiError(err: unknown): err is ApiError { return typeof err === 'object' && err !== null && 'code' in err; } - API response types. Generate from OpenAPI spec (
) or validate at the boundary with Zod. Never trust runtime data matches your types.openapi-typescript - Discriminated unions for state machines:
type AsyncState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'error'; error: Error } | { status: 'success'; data: T };
overas const
. Enums emit runtime code and have quirky behavior:enumconst ROLES = ['admin', 'editor', 'viewer'] as const; type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'
React 19
React 19 is the current stable release. Key additions:
New hooks:
— manages async form action state (replacesuseActionState(action, initialState)
). ReturnsuseFormState
.[state, formAction, isPending]
— in a child ofuseFormStatus()
, reads<form>
from the parent form. No prop drilling for loading state.{ pending, data, method, action }
— show optimistic UI immediately while an async action is pending. Reverts on error.useOptimistic(state, updateFn)
— read context or suspend on a promise inside render. Replaces someuse(promise | context)
/ async data patterns.useContext
function AddToCart({ productId }: { productId: string }) { const [state, formAction, isPending] = useActionState(addToCartAction, null); const [optimisticCart, addOptimistic] = useOptimistic( cart, (current, newItem: CartItem) => [...current, newItem], ); return ( <form action={async (formData) => { addOptimistic({ id: productId }); await formAction(formData); }}> <button disabled={isPending}>Add to cart</button> {state?.error && <span role="alert">{state.error}</span>} </form> ); }
Ref as prop (no more
forwardRef):
// React 19 — ref is a regular prop function Input({ ref, ...props }: React.ComponentProps<'input'>) { return <input ref={ref} {...props} />; }
Document metadata — render
<title>, <meta>, and <link> anywhere in the tree; React hoists them to <head>:
function ProductPage({ product }: { product: Product }) { return ( <> <title>{product.name} | Shop</title> <meta name="description" content={product.description} /> <h1>{product.name}</h1> </> ); }
React Compiler — automatically memoizes components and callbacks. When enabled, manual
useMemo, useCallback, and React.memo wrappers become largely unnecessary. Profile before adding manual memoization — the compiler may already handle it.
Server Components (RSC): in frameworks like Next.js App Router, components run on the server by default — no client JS, no hydration, direct DB/file access. Use
"use client" to mark the client boundary. Server Actions ("use server" async functions) handle mutations from Server Components without a separate API layer.
Testing
Unit tests
- Test behavior, not implementation. Interact like a user — click, type, assert visible output.
- Query by role, label, text — not by class name or test ID (last resort).
- Mock external dependencies (API, router, storage), not internal modules.
Integration tests
- Render full pages with mocked API (MSW). Test routing between pages, multi-step form flows, error states.
- Use a custom
that wraps providers (router, query client, theme).render
E2E tests
- Cover critical user paths: sign up, core workflow, payment. Keep the suite small (<50 tests) and fast (<5 minutes).
- Use Playwright. Page Object Model for reusable selectors.
- Run in CI against a staging environment or docker-compose stack.
Error Recovery
- Error boundaries. Wrap route segments with error boundaries. Show a fallback UI with a retry button — don't crash the entire page.
- Retry on failure. Configure TanStack Query with
and exponential backoff. Show a manual retry button after automatic retries are exhausted.retry: 3const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 3, retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), }, }, }); - Error pages. Dedicated 404 and 500 components. If using a framework with file-based routing, use its conventions (
,not-found.tsx
).error.tsx - Offline detection. Listen to
/online
events. Show a banner when offline. Warn users before actions that require network.offline - Graceful degradation. If a non-critical feature fails (analytics, chat widget, recommendations), catch the error and hide the feature. Don't crash the page for optional UI.
Security
- XSS. Never use
with user input. Sanitize with DOMPurify if you must render HTML.dangerouslySetInnerHTML - CSP. Set
header. At minimum:Content-Security-Policy
.default-src 'self'; script-src 'self' - CORS. Configure on the server, not the client. Never use
with credentials.Access-Control-Allow-Origin: * - Tokens. Store in
cookies, nothttpOnly
.localStorage
is readable by any script on the page.localStorage - Dependencies. Run
regularly. Usenpm audit
for production deps. Automate with Dependabot or Renovate.npm audit --omit=dev - SRI. Add
attribute to CDNintegrity
and<script>
tags.<link> - Error monitoring. Use Sentry or Datadog RUM to capture client-side errors in production. Configure source maps for readable stack traces.
Build Tooling
- Vite for new projects. Fast dev server (native ESM), optimized production builds (Rollup).
- Biome as an alternative to ESLint + Prettier. Single Rust-based tool for linting and formatting — faster, zero config for most projects. Evaluate for new projects; existing ESLint configs with custom rules may not have Biome equivalents yet.
- Path aliases.
in"@/*": ["./src/*"]
andtsconfig.json
:vite.config.ts// vite.config.ts resolve: { alias: { '@': path.resolve(__dirname, 'src') } } - Env vars.
files with.env
prefix for client-exposed variables. Never expose secrets —VITE_
vars are embedded in the bundle.VITE_ - Hashed filenames. Vite does this by default — enables aggressive caching.
- Source maps. Enable in production for error tracking (Sentry, Datadog). Upload maps privately, don't serve them publicly.
- CI pipeline:
→lint
→type-check
→test
→buildlighthouse
SEO Basics
- SSR/SSG for content that needs indexing. SPAs with client-side rendering are invisible to most crawlers.
- Unique
and<title>
per page. Title under 60 chars, description under 155.<meta name="description"> - Open Graph.
,og:title
,og:description
,og:image
for social previews.og:url - Canonical URL.
to avoid duplicate content.<link rel="canonical" href="..."> - JSON-LD. Structured data for rich results:
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "headline": "..." } </script>
+ sitemap.xml. Sitemap lists all indexable URLs. Submit to Google Search Console.robots.txt
New frontend app workflow
- [ ] Scaffold with Vite (React + TypeScript template) - [ ] Configure strict tsconfig, path aliases, ESLint, Prettier - [ ] Set up CSS strategy (CSS Modules or Tailwind) - [ ] Define design tokens (CSS custom properties) - [ ] Set up routing (React Router, TanStack Router) - [ ] Configure data fetching (TanStack Query) - [ ] Add testing stack (Vitest + Testing Library + MSW + Playwright) - [ ] Add error boundary at app root and error pages (404, 500) - [ ] Set up CI pipeline (lint → type-check → test → build → lighthouse) - [ ] Configure env vars, source maps, bundle analysis - [ ] Run validation loop (below)
Validation loop
— fix all warnings and errorsnpx eslint .
— fix type errorsnpx tsc --noEmit
— fix failing testsnpx vitest run
— fix E2E failuresnpx playwright test
ornpx axe-core
— fix accessibility violationsjest-axe
— verify bundle under 200KB gzippednpx vite build && npx vite-bundle-visualizer- Lighthouse CI — verify performance score ≥ 90, accessibility ≥ 95
- Repeat until all checks pass clean
Deep-dive references
Component patterns: See patterns/component-patterns.md for directory structure, composition, forms, error boundaries, compound components Performance patterns: See patterns/performance-patterns.md for profiling, code splitting, images, fonts, caching, rendering optimization Testing patterns: See patterns/testing-patterns.md for Vitest setup, component tests, MSW mocking, Playwright E2E Accessibility: See accessibility-cheatsheet.md for WCAG checklist, semantic HTML, ARIA reference, keyboard patterns
Official references
- WCAG 2.2 — Web Content Accessibility Guidelines, Level AA target
- web.dev — Core Web Vitals, performance, best practices
- Testing Library — query priorities, best practices, framework integrations
- Vite — configuration, plugins, build optimization