| name | frontend-components |
| description | Use when implementing or scaffolding a reusable UI component in React, Vue, Svelte, or Web Components/Lit; when porting a design across frameworks; or when a component needs hooks, composables, reactive state, slots, or shadow-DOM encapsulation. |
| metadata | {"references":["references/react-patterns.md","references/svelte-patterns.md","references/vue-patterns.md","references/web-components.md"]} |
Frontend Components
Framework selection (one-line picker)
| Need | Pick |
|---|
| Largest ecosystem, RSC/Next.js | React |
| Low-boilerplate SFCs, Nuxt | Vue |
| Smallest bundle, compiler-driven | Svelte |
| Framework-agnostic / design-system primitive | Web Components (Lit) |
Deep comparison: react-patterns.md, vue-patterns.md, svelte-patterns.md, web-components.md.
Workflow: build a new component
- Define props API — name, types, required vs optional, default values, controlled-vs-uncontrolled pattern (accept optional
value + onChange).
- Render markup — semantic HTML first, then styling.
- Wire state/effects — local state for ephemeral UI; lift to parent for shared.
- Add a11y — role, aria-*, keyboard handlers, focus management. Verify with axe DevTools.
- Write tests — render, interaction, a11y snapshot.
- Document — Storybook story per variant + states (default, loading, error, disabled).
Checkpoint after step 4: run npx @axe-core/cli http://localhost:6006/iframe?id=<story> — fail build if any violation.
Checkpoint after step 5: branch coverage on the component file must be >= 80% (vitest run --coverage).
Reference template (React + Suspense + custom hook)
import { Suspense, use, useEffect, useId, useState } from "react";
function useDebounced<T>(value: T, ms = 300): T {
const [v, setV] = useState(value);
useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]);
return v;
}
export function SearchBox({ value, onChange, resultsPromise }: {
value: string; onChange: (s: string) => void; resultsPromise: Promise<string[]>;
}) {
const id = useId();
const debounced = useDebounced(value);
return (
<div role="search">
<label htmlFor={id}>Search</label>
<input id={id} value={value} onChange={(e) => onChange(e.target.value)} aria-describedby={`${id}-hint`} />
< =<>Loading…}>
);
}
() {
items = (promise);
;
}
Vue SFC, Svelte 5 runes, and Lit equivalents (same controlled-input + debounce pattern) live in vue-patterns.md, svelte-patterns.md, and web-components.md.
Pre-merge gates
npx size-limit
npx @axe-core/cli http://localhost:6006/iframe.html?id=<story>
vitest run --coverage
Long lists (>100 rows): use react-window / vue-virtual-scroller / svelte-virtual-list. Code-split modal/admin features (React.lazy, defineAsyncComponent, dynamic import()).
Next Steps