Frontend Conventions
Frontend Conventions (apps/web)
Section titled “Frontend Conventions (apps/web)”Applies to the React/TanStack Start client. Server code (apps/web/src/server/**)
follows architecture-conventions.md / service-conventions.md instead.
Library-first — do NOT roll your own
Section titled “Library-first — do NOT roll your own”Prefer a maintained library over bespoke code wherever one fits. The goal is to write as little custom UI plumbing as possible. Reach for these before hand-rolling anything:
| Need | Use | Do NOT hand-roll |
|---|---|---|
| Styling | Tailwind CSS (v4) | bespoke CSS modules / inline style objects |
| Components | shadcn/ui (copy-in, lives in @/components/ui) | custom button/input/dialog/select/table/tabs/badge/toast |
| Primitives shadcn lacks | Radix UI (@radix-ui/*) | custom popover/menu/tooltip/accessible focus mgmt |
| Animation | Framer Motion | manual CSS keyframes / requestAnimationFrame |
| Server data + mutations | TanStack Query | bespoke fetch-in-useEffect + manual cache |
| Routing | TanStack Router (file-based, already set up) | manual route guards outside loaders |
| Icons | lucide-react | inline SVG paths |
| Class merging | clsx + tailwind-merge (cn util) | string concatenation of classNames |
| Auth (client) | better-auth/react createAuthClient | manual cookie/session handling |
Only build a custom component when no maintained library covers the need — and when you do, say why in the PR / handover. A new runtime UI dependency that isn’t in the table above: flag it for the user before adding.
Animation restraint
Section titled “Animation restraint”Framer Motion is for subtle, purposeful motion (page/sidebar transitions,
modal enter/exit, list reorder). Keep it understated — it must not distract from
data entry. Always respect prefers-reduced-motion (gate motion on it).
Component sourcing
Section titled “Component sourcing”- shadcn components are added via its CLI and committed under
@/components/ui. Edit the copied source when you need a variant — that’s the shadcn model — but don’t fork a component just to restyle what a Tailwind class would do. - Keep page/feature components in
apps/web/src/components/<feature>/; keep the shadcn primitives in@/components/ui/untouched except for intentional variants.
Data flow
Section titled “Data flow”- Fetch through TanStack Query against the typed
/api/v1client; invalidate on mutation. No business logic in components — call the API, which calls@packages/service(seearchitecture-conventions.md). - Auth-gate admin routes in the route loader (redirect to
/loginwhen unauthenticated), not inside component render.
Test-friendly components (Playwright)
Section titled “Test-friendly components (Playwright)”Business-flow components MUST be Playwright-addressable. Full spec + rationale:
plans/ARCHITECTURE.md §12.4 (source of truth — wins on any conflict).
data-testid="<feature>__<element>__<action>"— kebab-case, double-underscore separator. e.g.user-create__form__submit,user-archive__confirm-yes,user-create__email-input.- Mandatory on action surfaces: form submit/cancel, primary actions (Save / Delete / Approve / Reject / Cancel Sesi…), validation-gating inputs, modal confirm/cancel, view-changing tabs/segments, list-row action triggers, critical nav (logout, dashboard tabs). Optional on decorative, read-only display, layout chrome.
- State via ARIA, never an extra
data-qa-state—aria-busy(async loading),aria-invalid(bad input),aria-disabled,aria-expanded,aria-selected. shadcn/Radix already set these for accessibility; reusing them means tests also catch a11y regressions. - Select by testid, never by CSS class:
page.getByTestId('user-create__form__submit')— NOTpage.click('button.btn-primary'). - ESLint enforcement (
require-testid-on-action-buttons) is iter-2, not active yet — until then it is a review-time convention.
Dependency policy
Section titled “Dependency policy”Web-only UI libs stay in apps/web/package.json (most of the table above). Only
promote to the Bun catalog if a 2nd workspace also uses the package — see
deps.md.