Skip to content

ADR-0009: Frontend stack stays Next.js 16 + React 19 + Tailwind 4; add shadcn/ui as the component library

Context

RFC-0004 compared shadcn/ui against Mantine, Chakra, MUI, Ant Design, Park UI, Headless UI, DaisyUI, and "pick nothing — keep raw Tailwind + Radix." The framework cluster (Next.js 16, React 19, Tailwind 4) was confirmed as best-in-class for the goals: fast first paint, Cloudflare-Beirut-friendly, library-hospitable, MASTER's "components I can use to make UI/UX decisions easier" ask.

The current state from tech-stack: 7 of 8 @radix-ui/* packages are pre-installed but unused, plus cva, clsx, tailwind-merge, lucide-react, motion, cmdk, recharts. Every dependency shadcn/ui needs is already in the repo — the keystone (the CLI that generates components into src/components/ui/) just hasn't been installed.

Decision

The frontend stack stays Next.js 16 + React 19 + Tailwind 4. Add shadcn/ui (component generation CLI) and Prettier with prettier-plugin-tailwindcss (class-sort consistency).

Storybook, Bun-as-package-manager, and TanStack Query are explicitly deferred.

What ships in the implementation PR

  • bunx shadcn@latest init — generate components.json, lib/utils.ts, base CSS variables in globals.css
  • M2 component set installed: Button, Card, Sheet, Dialog, DropdownMenu, Tabs, Sonner (toasts), Skeleton, Form, Input, Label, Select, Separator, Popover, Calendar, Combobox, DataTable
  • prettier + prettier-plugin-tailwindcss configured (.prettierrc with tailwindFunctions: ["cn", "cva"])
  • eslint-config-prettier to disable ESLint rules that conflict with Prettier
  • react-hook-form added as the form library (idiomatic shadcn pair); zod is already installed and unused (per audit)
  • @tanstack/react-table added when the first DataTable surface lands (currently no consumer; defer until #16)

What does NOT ship in this ADR's implementation

  • Storybook — defer until the Figma friend's review workflow is concrete and shows Storybook would be load-bearing. If yes, file a separate ticket.
  • Bun as package manager — re-evaluate after shadcn lands; current package-lock.json continues. Filed as a follow-up audit ticket when convenient.
  • TanStack Query — RSC + server actions handle current data fetching. Re-evaluate when #15 cart aggregator or #14 price drop alerts need optimistic updates / cache invalidation.
  • Dark mode — defer until #42 brand identity signals whether it's a brand requirement or a "nice to have."

Consequences

Positive

  • Activates the already-installed Radix packages. 7 of 8 become useful as primitives the moment shadcn generates Button, Dialog, DropdownMenu, Select, Slider, Tabs, Tooltip, Checkbox.
  • Zero runtime lock-in. Components live in our repo. We modify freely, fork any component the moment it stops fitting, swap individual ones for alternatives (Vaul drawers, Sonner toasts already shadcn-shipped).
  • Tailwind 4 native — no CSS-in-JS overhead. The Figma friend's tokens (set in globals.css @theme inline) recolour every shadcn component immediately, no runtime cost.
  • Figma-friend workflow alignment. Figma → shadcn is the most well-trodden translation path in the industry as of 2026. v0.dev (Vercel) generates shadcn-shaped React from Figma frames — the friend's review workflow can become "drop the export into v0, get shadcn code, paste, tweak."
  • Smaller bundle. Tree-shakeable per-component (only ship what you add).
  • Bootstrap seam. New contributors run bunx shadcn add <component> and the component is in their working tree in <30s.
  • Sane defaults that aren't AI-slop. shadcn's neutral starting point gives us a base for the Figma friend to reskin against #42's brand decisions.

Negative

  • Component code lives in our repo, so we maintain it. Upstream shadcn updates are not auto-pulled — running bunx shadcn diff <component> to see upstream changes is a manual habit.
  • Day-one installation is bunx shadcn add per component. Not a single npm i and you have everything. (Trade-off pays back on bundle size + forkability.)
  • Adds Prettier as a project dep. One more thing to keep aligned with editor settings. (Trade-off pays back on PR diff cleanliness.)
  • Doesn't ship a built-in dark-mode toggle. Tailwind config + next-themes integration we'd add ourselves when #42 calls for it.
  • Form (UC-12, UC-C) requires react-hook-form + zod. Both mainstream; zod already installed.

Neutral

  • Theming uses Tailwind config + CSS variables. The Figma friend's tokens have to be expressed in those primitives; single source of truth makes design ↔ code drift visible immediately.

Alternatives considered (briefly — full analysis in RFC-0004)

Alternative Rejected because
Mantine Emotion-based CSS-in-JS friction with RSC; locks in Mantine design language
Chakra UI Same Emotion / RSC issue; recognisable "Chakra look" takes effort to escape
MUI (Material-UI) Material Design IS the AI-slop default we explicitly reject (per ADR-0002 + project memory). Restyling MUI to not look like Material is a never-finished fight
Ant Design Visually corporate, even more so than MUI; localisation is China-first; CSS-in-JS internals
Park UI Smaller ecosystem, Ark UI primitives instead of Radix (already installed); no v0.dev analog
Headless UI Smaller component set than Radix (no Combobox, no DataTable foundation, less granular keyboard handling)
DaisyUI Locks in DaisyUI's visual language; doesn't pair with Radix; theming away from defaults is constant fight
"Pick nothing" Every PR re-litigates "what does our Button look like?". Figma friend lands with no shared vocabulary; every Figma frame becomes a translation job from scratch

Decisions on RFC-0004 open questions

  1. Initial component set: install the M2 polish list on the shadcn-init PR (listed above). Lazy-add others as features need them.
  2. Storybook yes/no: No, deferred. Re-evaluate if the Figma friend's review workflow shows Storybook would be load-bearing.
  3. Theming flow: Tailwind config patches (PR-based) — friend produces palette/typography/radii in Figma, lands as a globals.css @theme inline patch in the same PR with the Figma export referenced.
  4. Form library: react-hook-form + zod (idiomatic shadcn pair; both ecosystem-standard; zod already installed-but-unused).
  5. DataTable: @tanstack/react-table when the first surface lands (#16 admin tools). Don't pre-install.
  6. Dark mode: Deferred until #42 brand work decides it's a requirement.

Implementation surface

The actual shadcn init work belongs on a follow-up code ticket (filed alongside #27 design system v2). This ADR records the decision; the implementation lands when the design system tokens from #27 and the brand identity from #42 provide the theming inputs. Until then, ad-hoc <div> + Tailwind continues — but no new bespoke component-from-scratch work; new components must wait for the shadcn foundation.

References