UI patterns

Tailwind 4 conventions

  • Design tokens live in globals.css as 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 or aria-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 onClick on <div> — use <button> or <a>