Skip to main content
AC
Build & Implementation5.6 KBMIT licensed

ui-engineering-stack-rules

Rewritten from patterns in PatrickJS/awesome-cursorrules (CC0-1.0)

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.

  • engineering
  • stack
  • rules

SKILL.md

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

LayerDefault
FrameworkNext.js App Router, React 19, Server Components by default
LanguageTypeScript, strict: true, no any
StylingTailwind with CSS-variable design tokens, cn() for composition
Componentsshadcn/ui (owned source in components/ui/) on Radix, cva variants
Motionmotion (import from motion/react), reduced-motion respected
DataServer Components and server actions; client store only for real client state
Package managerbun (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.

More in Build & Implementation

All skills