ADR-0009: Frontend stack stays Next.js 16 + React 19 + Tailwind 4; add shadcn/ui as the component library¶
- Status: Accepted
- Date: 2026-04-28
- Deciders: MASTER
- Related: RFC-0004, #27, #28, #42, #34, tech-stack, ADR-0005 (two-track UC model)
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— generatecomponents.json,lib/utils.ts, base CSS variables inglobals.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-tailwindcssconfigured (.prettierrcwithtailwindFunctions: ["cn", "cva"])eslint-config-prettierto disable ESLint rules that conflict with Prettierreact-hook-formadded as the form library (idiomatic shadcn pair);zodis already installed and unused (per audit)@tanstack/react-tableadded 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.jsoncontinues. 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 addper component. Not a singlenpm iand 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-themesintegration we'd add ourselves when #42 calls for it. - Form (UC-12, UC-C) requires
react-hook-form+zod. Both mainstream;zodalready 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¶
- Initial component set: install the M2 polish list on the shadcn-init PR (listed above). Lazy-add others as features need them.
- Storybook yes/no: No, deferred. Re-evaluate if the Figma friend's review workflow shows Storybook would be load-bearing.
- Theming flow: Tailwind config patches (PR-based) — friend produces palette/typography/radii in Figma, lands as a
globals.css@theme inlinepatch in the same PR with the Figma export referenced. - Form library:
react-hook-form+zod(idiomatic shadcn pair; both ecosystem-standard;zodalready installed-but-unused). - DataTable:
@tanstack/react-tablewhen the first surface lands (#16 admin tools). Don't pre-install. - 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¶
- RFC-0004 — Frontend stack and component library
- Tech stack reference
- shadcn/ui
- v0.dev — Figma → shadcn translation tool
- ADR-0005 — Two-track UC model — Casual primary landing affects component set