---
name: ui-engineering-stack-rules
description: House conventions for the frontend stack, Next.js App Router, React 19, TypeScript strict, Tailwind tokens, shadcn/ui, and Motion. Use before writing any React so output reads as authored by one team, and when bootstrapping a new frontend. Not for accessibility rules: use web-accessibility-wcag. Not for component API design: use shadcn-component-architecture. Not for animation craft: use premium-motion-ui. Not for verification runs: use browser-visual-qa.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: rewritten from patterns in PatrickJS/awesome-cursorrules (CC0-1.0)
  category: Build & Implementation
---

# UI Engineering Stack Rules

Consistency is what makes a codebase read as authored instead of generated. These rules pick one way to do each thing. Load them before writing React; follow the repo's existing choices when they differ, these are defaults, not a mandate to rewrite.

## The stack

| Layer | Default |
|---|---|
| Framework | Next.js App Router, React 19, Server Components by default |
| Language | TypeScript, `strict: true`, no `any` |
| Styling | Tailwind with CSS-variable design tokens, `cn()` for composition |
| Components | shadcn/ui (owned source in `components/ui/`) on Radix, `cva` variants |
| Motion | `motion` (import from `motion/react`), reduced-motion respected |
| Data | Server Components and server actions; client store only for real client state |
| Package manager | bun (`bunx shadcn@latest add ...`) |

## Server vs client components

- Default to Server Components. They fetch data and ship no JS.
- Add `"use client"` only for interactivity: hooks, event handlers, browser APIs, Motion, R3F.
- Push the client boundary down the tree. The page stays server; the leaf button goes client. Never mark a page client because one element needs `onClick`.
- Fetch in Server Components or server actions. No `useEffect` fetching for what the server can render.
- Secrets stay server-side. Never import server-only modules into client files.

## Files and naming

- One component per file, `PascalCase.tsx`, helpers colocated.
- Hooks: `useThing.ts` in `hooks/`. Utilities: `lib/` (`lib/utils.ts` holds `cn()`).
- Routes follow App Router: `app/segment/page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`.
- Directories kebab-case, types PascalCase, constants UPPER_SNAKE_CASE.
- Named exports; default-export only where the router requires it.

## TypeScript

- No `any`. Use `unknown` plus narrowing, generics, or a real type.
- Props get an explicit `interface` or `type`; derive variant props via `VariantProps<typeof variants>`.
- Validate domain data with zod at the boundary; infer types with `z.infer`.
- Discriminated unions over boolean-flag soup for component states.

## Styling

- Tokens only: `bg-background`, `text-foreground`, `border-border`. Never raw palette classes like `bg-slate-900`. Restyling happens in tokens, not components.
- Compose through `cn()`; spread incoming `className` last so callers can override.
- New looks are new `cva` variants on the existing component, never a forked copy.
- Mobile-first: base styles, then `sm:` / `md:` / `lg:`. Check ~375px and ~1440px.
- Density is a decision. Real products are not uniformly `p-8 gap-6`.

## Avoiding the generic AI look

- No unmotivated purple gradients, oversized glassy cards, or centered-everything heroes.
- One strong accent from the token set beats a rainbow.
- A real type scale and one consistent `--radius` read as designed; random sizes read as generated.
- One signature motion or 3D moment per section, not effect soup (premium-motion-ui covers the craft).

## Good vs bad

**Bad:** A dashboard page marked `"use client"` at the top, data fetched in `useEffect`, cards styled with `bg-slate-800 rounded-2xl p-8`, a copied `ButtonSecondary.tsx` that is `Button` with different colors.

**Good:** Server page fetches via a server action, one `"use client"` leaf for the filter dropdown, cards on `bg-card border-border`, the secondary look added as a `variant: "secondary"` entry in Button's `cva` map.

## Verification

Run `bunx tsc --noEmit && bun run lint && bun run build`. Expect all three to exit 0 with no new warnings. Then grep for drift: `grep -rn "bg-slate-\|bg-gray-\|text-white " app/ components/ | grep -v components/ui` should return nothing, and `grep -rln '"use client"' app/**/page.tsx` should return nothing. Any hit: fix the offender before calling the surface done, then hand off to browser-visual-qa for the responsive and theme matrix.

## Completion checklist

- [ ] Server Components by default; client boundary pushed to leaves
- [ ] No `any`; props typed; zod at data boundaries
- [ ] Semantic tokens only; `className` merged last via `cn()`
- [ ] Variants in `cva`, no forked components
- [ ] Naming and file layout match the repo
- [ ] Empty, loading, and error states exist and look intentional
- [ ] tsc, lint, build clean; a11y and browser QA handed off

Any box unchecked: not done. Fix or say so.

## Footguns

- **The page-level `"use client"`.** One interactive widget drags the whole route to client rendering and kills the server data path. Extract the widget instead.
- **Raw palette classes "just this once".** The first `bg-slate-900` breaks theme switching for that surface and invites the second. Tokens or nothing.
- **Forking a component to restyle it.** Two sources of truth drift immediately. Add a variant; if the component cannot express it, fix the component (shadcn-component-architecture).
- **Repo mismatch.** Applying these defaults to a Vite or npm repo creates a second convention. The repo's existing choices win; these rules fill the gaps.
