---
title: "UI patterns"
url: https://mdfy.app/wp12y7sM
updated: 2026-05-16T14:17:17.723Z
source: "demo-seed"
---
# 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

```text
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>`
