# Frontend Standards > **⚠️ MAINTENANCE:** This file is indexed in `dev-team/skills/shared-patterns/standards-coverage-table.md`. > When adding/removing `## ` sections, follow FOUR-FILE UPDATE RULE in CLAUDE.md: (1) edit standards file, (2) update TOC, (3) update standards-coverage-table.md, (4) update agent file. This file defines the specific standards for frontend development. > **Reference**: Always consult `docs/PROJECT_RULES.md` for common project standards. --- ## Table of Contents | # | Section | Description | |---|---------|-------------| | 1 | [Framework](#framework) | React 18+, Next.js 14+ | | 2 | [Libraries & Tools](#libraries--tools) | Core, state, forms, UI, styling, testing | | 3 | [State Management Patterns](#state-management-patterns) | TanStack Query, Zustand | | 4 | [Form Patterns](#form-patterns) | React Hook Form + Zod | | 5 | [Styling Standards](#styling-standards) | TailwindCSS, CSS variables | | 6 | [Typography Standards](#typography-standards) | Font selection and pairing | | 7 | [Animation Standards](#animation-standards) | CSS transitions, Framer Motion | | 8 | [Component Patterns](#component-patterns) | Compound components, error boundaries | | 9 | [Accessibility](#accessibility) | WCAG 2.1 AA compliance | | 10 | [Performance](#performance) | Code splitting, image optimization | | 11 | [Directory Structure](#directory-structure) | Next.js App Router layout | | 12 | [Forbidden Patterns](#forbidden-patterns) | Anti-patterns to avoid | | 13 | [Standards Compliance Categories](#standards-compliance-categories) | Categories for dev-refactor | **Meta-sections (not checked by agents):** - [Checklist](#checklist) - Self-verification before submitting code --- ## Framework - React 18+ / Next.js 14+ - TypeScript strict mode (see `typescript.md`) --- ## Libraries & Tools ### Core | Library | Use Case | |---------|----------| | React 18+ | UI framework | | Next.js 14+ | Full-stack framework | | TypeScript 5+ | Type safety | ### State Management | Library | Use Case | |---------|----------| | TanStack Query | Server state (API data) | | Zustand | Client state (UI state) | | Context API | Simple shared state | | Redux Toolkit | Complex global state | ### Forms | Library | Use Case | |---------|----------| | React Hook Form | Form state management | | Zod | Schema validation | | @hookform/resolvers | RHF + Zod integration | ### UI Components | Library | Use Case | |---------|----------| | Radix UI | Headless primitives | | shadcn/ui | Pre-styled Radix components | | Chakra UI | Full component library | | Headless UI | Tailwind-native primitives | ### Styling | Library | Use Case | |---------|----------| | TailwindCSS | Utility-first CSS | | CSS Modules | Scoped CSS | | Styled Components | CSS-in-JS | | CSS Variables | Theming | ### Testing | Library | Use Case | |---------|----------| | Vitest | Unit tests | | Testing Library | Component tests | | Playwright | E2E tests | | MSW | API mocking | --- ## State Management Patterns ### Server State with TanStack Query ```typescript import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // Query key factory const userKeys = { all: ['users'] as const, lists: () => [...userKeys.all, 'list'] as const, list: (filters: UserFilters) => [...userKeys.lists(), filters] as const, details: () => [...userKeys.all, 'detail'] as const, detail: (id: string) => [...userKeys.details(), id] as const, }; // Typed query hook function useUser(userId: string) { return useQuery({ queryKey: userKeys.detail(userId), queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5 minutes }); } // Mutation with cache update function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: createUser, onSuccess: (newUser) => { // Update cache queryClient.setQueryData( userKeys.detail(newUser.id), newUser ); // Invalidate list queryClient.invalidateQueries({ queryKey: userKeys.lists(), }); }, }); } ``` ### Client State with Zustand ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface UIState { theme: 'light' | 'dark'; sidebarOpen: boolean; setTheme: (theme: 'light' | 'dark') => void; toggleSidebar: () => void; } const useUIStore = create()( persist( (set) => ({ theme: 'light', sidebarOpen: true, setTheme: (theme) => set({ theme }), toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), }), { name: 'ui-storage' } ) ); // Usage in component function Header() { const { theme, setTheme } = useUIStore(); return ; } ``` --- ## Form Patterns ### React Hook Form + Zod ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // Schema const createUserSchema = z.object({ name: z.string().min(1, 'Name is required').max(100), email: z.string().email('Invalid email'), role: z.enum(['admin', 'user', 'guest']), notifications: z.boolean().default(true), }); type CreateUserInput = z.infer; // Component function CreateUserForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(createUserSchema), defaultValues: { notifications: true, }, }); const createUser = useCreateUser(); const onSubmit = async (data: CreateUserInput) => { await createUser.mutateAsync(data); }; return (
); } ``` --- ## Styling Standards ### TailwindCSS Best Practices ```tsx // Use semantic class groupings
// Extract repeated patterns to components function Card({ children, className }: CardProps) { return (
{children}
); } ``` ### CSS Variables for Theming ```css :root { --color-primary: 220 90% 56%; --color-secondary: 262 83% 58%; --color-background: 0 0% 100%; --color-foreground: 222 47% 11%; --color-muted: 210 40% 96%; --color-border: 214 32% 91%; --radius: 0.5rem; } .dark { --color-background: 222 47% 11%; --color-foreground: 210 40% 98%; --color-muted: 217 33% 17%; --color-border: 217 33% 17%; } ``` ### Mobile-First Responsive Design ```tsx // Always start mobile, scale up
// Responsive text

// Hide/show based on breakpoint
Desktop only
Mobile only
``` --- ## Typography Standards ### Font Selection (AVOID GENERIC) ```tsx // FORBIDDEN - Generic AI fonts font-family: 'Inter', sans-serif; // Too common font-family: 'Roboto', sans-serif; // Too common font-family: 'Arial', sans-serif; // System font font-family: system-ui, sans-serif; // System stack // RECOMMENDED - Distinctive fonts font-family: 'Geist', sans-serif; // Modern, tech font-family: 'Satoshi', sans-serif; // Contemporary font-family: 'Cabinet Grotesk', sans-serif; // Bold, editorial font-family: 'Clash Display', sans-serif; // Display headings font-family: 'General Sans', sans-serif; // Clean, versatile ``` ### Font Pairing ```css /* Display + Body pairing */ --font-display: 'Clash Display', sans-serif; --font-body: 'Satoshi', sans-serif; /* Heading uses display */ h1, h2, h3 { font-family: var(--font-display); } /* Body uses readable font */ body, p, span { font-family: var(--font-body); } ``` --- ## Animation Standards ### CSS Transitions (Simple Effects) ```css /* Standard transition */ .button { transition: all 150ms ease; } /* Specific properties for performance */ .card { transition: transform 200ms ease, box-shadow 200ms ease; } /* Hover states */ .card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } ``` ### Framer Motion (Complex Animations) ```tsx import { motion, AnimatePresence } from 'framer-motion'; // Page transitions function PageWrapper({ children }: { children: React.ReactNode }) { return ( {children} ); } // Staggered list animation function ItemList({ items }: { items: Item[] }) { return ( {items.map((item, i) => ( {item.name} ))} ); } ``` ### Animation Guidelines 1. **Focus on high-impact moments** - Page loads, modal opens, state changes 2. **One orchestrated animation > scattered micro-interactions** 3. **Keep durations short** - 150-300ms for UI, 300-500ms for page transitions 4. **Use easing** - `ease`, `ease-out` for exits, `ease-in-out` for continuous --- ## Component Patterns ### Compound Components ```tsx // Flexible API for complex components function Tabs({ children, defaultValue }: TabsProps) { const [value, setValue] = useState(defaultValue); return (
{children}
); } Tabs.List = function TabsList({ children }: { children: React.ReactNode }) { return
{children}
; }; Tabs.Trigger = function TabsTrigger({ value, children }: TabsTriggerProps) { const { value: selected, setValue } = useTabsContext(); return ( ); }; Tabs.Content = function TabsContent({ value, children }: TabsContentProps) { const { value: selected } = useTabsContext(); if (value !== selected) return null; return
{children}
; }; // Usage Tab 1 Tab 2 Content 1 Content 2 ``` ### Error Boundaries ```tsx import { Component, ErrorInfo, ReactNode } from 'react'; interface Props { children: ReactNode; fallback: ReactNode; } interface State { hasError: boolean; } class ErrorBoundary extends Component { state: State = { hasError: false }; static getDerivedStateFromError(): State { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Error:', error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } // Usage }> ``` --- ## Accessibility ### Required Practices ```tsx // Always use semantic HTML // not
// Images need alt text {`${user.name}'s // Form inputs need labels // Use ARIA when needed // Keyboard navigation
e.key === 'Enter' && onClick()} onClick={onClick} > ``` ### Focus Management ```tsx // Focus trap for modals import { FocusTrap } from '@radix-ui/react-focus-scope'; ... // Auto-focus on mount const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); ``` --- ## Performance ### Code Splitting ```tsx import { lazy, Suspense } from 'react'; // Lazy load heavy components const Dashboard = lazy(() => import('./pages/Dashboard')); const Analytics = lazy(() => import('./pages/Analytics')); // Use Suspense }> ``` ### Image Optimization ```tsx import Image from 'next/image'; // Always use next/image {user.name} ``` ### Memoization ```tsx // Memo expensive components const ExpensiveList = memo(function ExpensiveList({ items }: Props) { return items.map(item => ); }); // useMemo for expensive calculations const sortedItems = useMemo( () => items.sort((a, b) => b.score - a.score), [items] ); // useCallback for stable references const handleClick = useCallback((id: string) => { setSelectedId(id); }, []); ``` --- ## Directory Structure ```text /src /app # Next.js App Router /api # API routes /(auth) # Auth route group /(dashboard) # Dashboard route group layout.tsx page.tsx /components /ui # Primitive UI components button.tsx input.tsx card.tsx /features # Feature-specific components /user UserProfile.tsx UserList.tsx /order OrderForm.tsx /hooks # Custom hooks useUser.ts useDebounce.ts /lib # Utilities api.ts utils.ts cn.ts /stores # Zustand stores userStore.ts uiStore.ts /types # TypeScript types user.ts api.ts /public # Static assets ``` --- ## Forbidden Patterns **The following patterns are never allowed. Agents MUST refuse to implement these:** ### TypeScript Anti-Patterns | Pattern | Why Forbidden | Correct Alternative | |---------|---------------|---------------------| | `any` type | Defeats TypeScript purpose | Use proper types, `unknown`, or generics | | Type assertions without validation | Runtime errors | Use type guards or Zod parsing | | `// @ts-ignore` or `// @ts-expect-error` | Hides real errors | Fix the type issue properly | | Non-strict mode | Allows unsafe code | Enable `"strict": true` in tsconfig | ### Accessibility Anti-Patterns | Pattern | Why Forbidden | Correct Alternative | |---------|---------------|---------------------| | `
` for buttons | Not keyboard accessible | Use `