Comprehensive guidelines for implementing clean, consistent, accessible, and performant interaction patterns in React/Next.js applications using TypeScript.
Your components break when users behave unexpectedly. Loading states flicker inconsistently. Error boundaries crash the entire page. Accessibility is an afterthought that breaks screen readers.
Sound familiar? You're not alone – most React applications ship with interaction patterns that feel cobbled together, perform poorly, and fail real users when it matters most.
Every React developer faces the same core challenges when building user interfaces:
The result? Users abandon your application before completing critical actions, your team spends weeks debugging interaction edge cases, and your codebase becomes a maintenance nightmare.
These Cursor Rules establish a comprehensive framework for building interaction patterns that actually work – patterns that are accessible by default, perform under pressure, and scale with your product complexity.
Instead of fighting React's quirks, you'll leverage a battle-tested system that handles:
// Before: Brittle interaction handling
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async () => {
setLoading(true);
try {
await submitForm();
// What happens if component unmounts here?
setLoading(false);
} catch (err) {
setError(err.message); // Still loading = true!
setLoading(false);
}
};
// After: Bulletproof typed interaction
const { status, run } = useAsync();
const handleSubmit = () => run(submitForm());
// Automatically handles loading, error, and cleanup
Stop spending hours tracking down impossible UI states. The rules enforce discriminated unions that make invalid states literally impossible to represent:
type SubmissionState =
| { status: 'idle' }
| { status: 'submitting' }
| { status: 'success'; data: Response }
| { status: 'error'; error: Error };
Impact: Cut debugging time by 60% and ship features with confidence that edge cases are handled.
Accessibility becomes automatic with built-in focus management, ARIA live regions, and color contrast validation in your CI pipeline:
// Accessibility patterns baked into every component
<AsyncButton onAction={handleSubmit}>
Submit Order
</AsyncButton>
// Includes focus ring, loading state, ARIA attributes, and screen reader support
Impact: Pass accessibility audits on the first try and reach 100% of your users.
Performance optimization becomes systematic with automatic code splitting, memoization guidelines, and bundle size monitoring:
next/dynamicImpact: Maintain fast interactions even as your application grows to hundreds of components.
Before: Manually managing form state, validation errors, loading states, and accessibility across multiple components.
After: Single useAsync hook with shared Zod schemas provides client/server validation, typed error handling, and accessible feedback – all in 3 lines of code.
Before: Fighting focus management, escape key handling, and screen reader compatibility while building custom overlay components.
After: Drop-in @headlessui/react components with automatic focus trapping and ARIA compliance, customized with your design system tokens.
Before: Scattered loading spinners, inconsistent error displays, and stale data causing UI bugs.
After: Centralized async state management with typed error boundaries, consistent loading patterns, and automatic race condition prevention.
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"exactOptionalPropertyTypes": true
}
}
npm install @headlessui/react xstate @xstate/react zod
npm install -D @axe-core/react size-limit
// hooks/useAsync.ts
export const useAsync = <T>() => {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
const run = useCallback(async (promise: Promise<T>) => {
setState({ status: 'pending' });
try {
const data = await promise;
setState({ status: 'success', data });
} catch (error) {
setState({ status: 'error', error: error as Error });
}
}, []);
return { ...state, run };
};
Start with the provided AsyncButton component and extend the pattern to forms, modals, and data tables.
# .github/workflows/quality.yml
- name: Accessibility Check
run: npm run test:a11y
- name: Bundle Size Limit
run: npm run size-limit
- name: Type Check
run: npm run type-check
These rules don't just improve your code – they fundamentally change how fast you can ship reliable, accessible, high-performance React applications. You'll spend less time debugging edge cases and more time building features that drive your business forward.
Your users will notice the difference immediately: faster load times, consistent feedback, and interfaces that work perfectly with assistive technologies. Your team will notice the difference in every sprint: fewer bugs, faster development cycles, and confidence that new features won't break existing functionality.
Ready to transform your React development workflow? Implement these rules and experience the difference that systematic interaction design makes in your daily development life.
You are an expert in TypeScript, React (with Next.js), Tailwind CSS, Jest, React Testing Library, GitHub Actions.
Key Principles
- Design with the user: run discovery interviews, build personas, prototype, and validate every critical interaction with moderated usability testing.
- Keep all interactions simple and self-evident; each screen should answer one question: “What can I do here?”
- Enforce visual and behavioural consistency: colours, typography, iconography, motion curves, and feedback timing must match the design system.
- Provide immediate, multi-modal feedback (visual, ARIA live-regions, haptics on mobile) for every user action within 150 ms.
- Accessibility is non-negotiable: WCAG 2.2 AA minimum.
- Optimise for speed: ship <100 KB critical JS, lazy-load non-critical chunks, and keep interaction latency below 100 ms.
- Instrument everything: add analytics hooks to all key interactions and feed learnings back into iterative design.
TypeScript (language-specific rules)
- Use strict mode (`"strict": true`) and disallow `any`; prefer `unknown` + type-guards.
- Define UI data with `interface` (components) and `type` (utility unions).
- Function order inside files: constants → utility functions → hooks → component → export.
- Always return typed discriminated unions for state machines (`{ state: 'idle' } | { state: 'loading' } | { state: 'error'; error: Error }`).
- Name booleans with auxiliary verbs (`isSubmitting`, `hasError`).
- Use `async/await`; never mix with `.then()` in the same scope.
- Prefer `ReadonlyArray<T>` over `T[]` when mutation is not required.
Error Handling & Validation
- Detect and handle edge cases at the top of the function with early returns.
- Wrap async UI actions in a typed `useAsync` hook that returns `{status, data, error}`.
- Display errors through a non-blocking toast and inline message near the origin control.
- Add a global `<ErrorBoundary>` per route; reset boundary when the route changes.
- Validate user input client-side (zod) first, then server-side; keep schema shared via `@/schema` package.
- Log caught errors to Sentry with user identifier (GDPR-compliant).
React / Next.js (framework rules)
- Use functional components; avoid class components.
- Co-locate component, test, story, and styles in the same folder.
- File naming: `kebab-case.tsx` for visual components, `useThing.ts` for hooks, `*-machine.ts` for statecharts.
- Use `next/link` & `next/router` for navigation; never manipulate `window.location` directly.
- Derive state; avoid duplicating server data in client cache (`swr` or `react-query`).
- Manage complex UI flows with XState; persist current state in the URL (`/checkout?step=payment`).
- Hide server interactions behind service functions (`@/services/cart.ts`) returning typed DTOs.
- Prefetch data in `getServerSideProps` for authenticated pages, `getStaticProps` for public ones.
- Theme via Tailwind CSS; never write raw hex codes, rely on config tokens (`text-primary`).
Accessibility
- Every interactive element must have a visible focus ring (`focus:outline-primary`).
- ARIA roles only when semantic HTML is insufficient; never override native roles.
- Use `aria-live="polite"` regions for background status updates (e.g., file upload progress).
- Ensure colour contrast ≥ 4.5:1; verify in CI with `axe-core`.
Performance
- Use `next/dynamic` with `{ suspense: true }` to lazy-load heavy widgets.
- Memoise pure components with `React.memo` and hooks with `useMemo` only when re-render cost > compute cost.
- Serve images through `next/image` with adaptive sizes and AVIF/WebP.
- Run `yarn size-limit` in CI; budget: JS < 200 KB gzip per route.
Testing
- Unit test every pure utility with Jest; aim for 100% branch coverage on critical algorithms.
- Interaction tests with React Testing Library: prioritise user events (`userEvent.click(...)`) over implementation details.
- End-to-end flows in Cypress for primary journeys; include accessible checks (`cy.checkA11y()`).
- Snapshot only presentational components; avoid logic snapshots.
Analytics & Telemetry
- Wrap key interactions in a `track` helper: `track('signup.submit', { plan })`.
- Store user consent in `localStorage`; disable tracking if not granted.
- Send performance data (FID, CLS, INP) to Lighthouse CI dashboard.
CI/CD
- Pull requests must pass: lint (`eslint --max-warnings=0`), type-check, test, size-limit, axe-core a11y, Lighthouse budget.
- Auto-deploy preview to Vercel; run visual regression tests with Chromatic.
Directory Conventions
- `app/` Next 13 app router pages.
- `components/` shareable UI pieces.
- `features/` domain-oriented slices (`features/checkout`) containing screens, services, machines, tests.
- `hooks/` reusable logic.
- `styles/` Tailwind config & global CSS.
- `lib/` cross-cutting utilities (analytics, i18n, http client).
Common Pitfalls & Remedies
- Pitfall: Duplicate loading spinners. Remedy: Centralise loading state in parent and pass as props.
- Pitfall: Missing focus management on modal open. Remedy: Use `@headlessui/react` dialog that traps focus automatically.
- Pitfall: Unhandled async race conditions (e.g., stale form submission). Remedy: Cancel previous requests in `useAsync` with AbortController.
Ready-to-use Snippet — Async Action Button
```tsx
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onAction: () => Promise<void>;
}
export const AsyncButton: React.FC<AsyncButtonProps> = ({ onAction, children, ...rest }) => {
const { status, run } = useAsync();
return (
<button
{...rest}
disabled={status === 'pending' || rest.disabled}
onClick={() => run(onAction())}
className="relative px-4 py-2 font-medium text-white bg-primary rounded focus:outline-primary disabled:opacity-50"
>
{status === 'pending' && <Spinner className="absolute left-2" />}
<span className={status === 'pending' ? 'opacity-0' : undefined}>{children}</span>
</button>
);
};
```
Follow this rule set to deliver predictable, inclusive, and fast interaction patterns that delight users and scale with your product.