From chogos
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/chogos:developing-frontend-appsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- One component, one job. If it fetches data AND renders a complex UI, split it.
interface UserCardProps {
user: User;
onEdit: (id: string) => void;
variant?: 'compact' | 'full';
}
function UserCard({ user, onEdit, variant = 'full' }: UserCardProps) {
// ...
}
useState or useReducer for component-scoped concerns.// 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]
);
useState per field doesn't scale past 3-4 fields — validation, dirty tracking, and error display become unwieldy.structuredClone — never mutate state directly.lazy(). Lazy-load heavy components (editors, charts, maps).npx vite-bundle-visualizer or source-map-explorer to find bloat.startTransition for expensive updates.React.memo and stable callback references with useCallback. Profile first — premature memoization adds complexity without measurable gain.loading="lazy" and always set width/height attributes.<nav>, <main>, <aside>, <section>, <header>, <footer>. Native interactive elements over styled divs.alt="" for decorative images.<label>. Link error messages with 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>}
<button> already has role="button" — don't add it again.@axe-core/cli or vitest-axe in CI..module.css) or Tailwind utility classes. Avoid global stylesheets beyond reset/tokens.: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;
}
@media (min-width: ...) for larger.margin-inline, padding-block, inline-size instead of directional properties — supports RTL layouts.em/rem, or calc(). Every value should have a reason.gap on flex/grid replaces margin hacks and adjacent sibling selectors.@media (prefers-reduced-motion: no-preference). Provide a static alternative for users with vestibular disorders.
@media (prefers-reduced-motion: no-preference) {
.card { transition: transform 0.2s ease; }
.card:hover { transform: scale(1.02); }
}
transform and opacity — they run on the compositor thread, avoiding layout/paint. Use will-change sparingly and remove after animation completes..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); }
}
.card-container { container-type: inline-size; }
@container (min-width: 400px) {
.card { grid-template-columns: 1fr 2fr; }
}
<button popovertarget="menu">Open</button>
<div id="menu" popover>Popover content</div>
@starting-style. Entry animations for elements transitioning from display: none or entering the DOM:
dialog[open] {
opacity: 1;
transition: opacity 0.3s;
@starting-style { opacity: 0; }
}
document.startViewTransition(). Pair with view-transition-name CSS property to animate specific elements between states.strict: true in tsconfig.json. No exceptions.interface SearchState {
query: string;
results: SearchResult[];
status: 'idle' | 'loading' | 'error' | 'success';
}
any. Use unknown and narrow with type guards:
function isApiError(err: unknown): err is ApiError {
return typeof err === 'object' && err !== null && 'code' in err;
}
openapi-typescript) or validate at the boundary with Zod. Never trust runtime data matches your types.type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: Error }
| { status: 'success'; data: T };
as const over enum. Enums emit runtime code and have quirky behavior:
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'
React 19 is the current stable release. Key additions:
New hooks:
useActionState(action, initialState) — manages async form action state (replaces useFormState). Returns [state, formAction, isPending].useFormStatus() — in a child of <form>, reads { pending, data, method, action } from the parent form. No prop drilling for loading state.useOptimistic(state, updateFn) — show optimistic UI immediately while an async action is pending. Reverts on error.use(promise | context) — read context or suspend on a promise inside render. Replaces some useContext / async data patterns.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.
render that wraps providers (router, query client, theme).retry: 3 and exponential backoff. Show a manual retry button after automatic retries are exhausted.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
},
},
});
not-found.tsx, error.tsx).online/offline events. Show a banner when offline. Warn users before actions that require network.dangerouslySetInnerHTML with user input. Sanitize with DOMPurify if you must render HTML.Content-Security-Policy header. At minimum: default-src 'self'; script-src 'self'.Access-Control-Allow-Origin: * with credentials.httpOnly cookies, not localStorage. localStorage is readable by any script on the page.npm audit regularly. Use npm audit --omit=dev for production deps. Automate with Dependabot or Renovate.integrity attribute to CDN <script> and <link> tags."@/*": ["./src/*"] in tsconfig.json and vite.config.ts:
// vite.config.ts
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
}
.env files with VITE_ prefix for client-exposed variables. Never expose secrets — VITE_ vars are embedded in the bundle.lint → type-check → test → build → lighthouse<title> and <meta name="description"> per page. Title under 60 chars, description under 155.og:title, og:description, og:image, og:url for social previews.<link rel="canonical" href="..."> to avoid duplicate content.<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "Article", "headline": "..." }
</script>
robots.txt + sitemap.xml. Sitemap lists all indexable URLs. Submit to Google Search Console.- [ ] 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)
npx eslint . — fix all warnings and errorsnpx tsc --noEmit — fix type errorsnpx vitest run — fix failing testsnpx playwright test — fix E2E failuresnpx @axe-core/cli or vitest-axe in tests — fix accessibility violationsnpx vite build && npx vite-bundle-visualizer — verify bundle under 200KB gzippedComponent 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
npx claudepluginhub chogos/claude-skills --plugin chogosBuilds frontend components, optimizes performance and bundle sizes, scaffolds projects, implements accessibility, reviews code quality, and designs UI/UX across React, Vue, Svelte, and other frameworks.
Guides frontend UI/UX development with patterns for responsive design, accessibility, component architecture, state management, visual consistency, performance, and testing. Useful for any frontend task.
Scaffolds React/Next.js projects, generates components, analyzes bundle sizes, and reviews frontend patterns, performance, and accessibility.