UI patterns
Tailwind 4 conventions
- Design tokens live in
globals.cssas CSS variables:--background,--accent,--text-primary, etc. - Use
var(--...)everywhere. Don't hardcode hex. - Dark mode via
[data-theme="dark"]on<html>. Tokens get re-bound at the document root.
Component organization
components/
├── atoms/ # Button, Input, Badge — no state, no fetching
├── molecules/ # FormField, Card, Menu — local state OK
├── organisms/ # PageEditor, BlockToolbar — owns fetching
└── primitives/ # shadcn/ui exports (rename to mark these as ours)
Rule: atoms can be used everywhere. Organisms can't be imported by atoms or molecules — only by pages.
Form validation
Zod schemas live next to the form. useForm from React Hook Form.
Server validates the SAME schema. Don't trust client validation
alone — the API enforces it again.
Loading states
Prefer <Suspense> boundaries with skeletons over conditional
isLoading ternaries. Skeletons match the final layout so the
viewport doesn't jump.
Empty states
Every list view has an empty state with: icon + 1-line copy + 1 CTA. Don't ship a blank screen — that's a bug.
Accessibility
- Every
<button>has either visible text oraria-label - Color contrast WCAG AA minimum (we use the Tailwind defaults which pass AA for body text)
- Keyboard nav works — every interactive element reachable via Tab, modal traps focus, Escape closes overlays
Things to avoid
- No inline styles unless you're animating (transform/opacity only)
- No
!important— if you need it, the cascade is wrong - No
onClickon<div>— use<button>or<a>