RFC-0004: Frontend stack and component library¶
- Status: Accepted — locked by ADR-0009 on 2026-04-28
- Author: MASTER (drafted by Claude as part of #34)
- Date: 2026-04-28
- Related: #27 design system v2, #28 page designs, #42 brand identity, ADR-0009, tech-stack reference, Personas
Decision recorded. Next.js 16 + React 19 + Tailwind 4 stays; add shadcn/ui + Prettier. Storybook, Bun, TanStack Query deferred. See ADR-0009 for the locked decision; this RFC remains as the comparative analysis that produced it.
Summary¶
961tech's frontend currently sits on Next.js 16 + React 19 + Tailwind 4 — the right framework cluster for our goals (fast initial paint, RSC, Lebanon-latency-friendly, library-hospitable). It's missing a component library layer: 7 of 8 @radix-ui/* packages are pre-installed but unused (see tech-stack § UI primitives). Those Radix packages are exactly the primitives shadcn/ui wraps; the foundation is in the repo, the keystone isn't. Recommendation: initialise shadcn/ui (no new runtime dependency — it generates components into src/components/ui/), add Prettier + prettier-plugin-tailwindcss to keep class ordering deterministic as the component count grows, and defer Storybook unless the Figma friend's review workflow shows it'd be load-bearing.
Motivation¶
The Casual + Builder two-track model (ADR-0005) means #28 page design will produce many Figma frames that need to translate into code quickly and consistently. The current state pushes that work onto raw <div>s + Tailwind, which:
- Loses the constraint Radix already provides (focus management, keyboard interactions, ARIA wiring) — the Radix packages are installed for exactly this reason but never activated
- Forces a per-component design decision for things that should be solved-once (Button variants, Card spacing, Dialog overlay shape)
- Slows down the Figma-friend → code handoff because there's no shared vocabulary between design tokens and code primitives
- Makes brand-identity work (#42) re-litigate the same micro-decisions for every component instead of theming a coherent system
The framework pieces themselves are not contested — Next 16 + React 19 + Tailwind 4 is best-in-class for full-stack React apps targeting fast first paint, MENA latency profiles, and small-team velocity. This RFC primarily addresses the missing component-library layer.
Proposal¶
Confirm: framework stays as-is¶
| Layer | Choice | Why it stays |
|---|---|---|
| Framework | Next.js 16 (App Router, RSC) | Server-first rendering minimises Beirut TTFB; RSC + streaming pairs with the RFC-0001 Cloudflare Workers target; next/font, next/image, next/link give us perf budgets for free |
| UI library | React 19 | RSC requires React 19; the rest of the ecosystem has caught up |
| Styling | Tailwind 4 (@theme inline in globals.css) |
Lightning CSS engine is materially faster than v3; CSS-first config makes the Figma-friend's tokens trivially translatable; no runtime CSS-in-JS overhead (Mantine/Chakra would re-introduce it) |
No alternatives evaluated for these — the current cluster is the right one for the goals. See §Alternatives for the libraries that would replace shadcn/ui specifically.
Recommendation: add shadcn/ui¶
shadcn/ui is a component-generation CLI built on Tailwind + Radix + cva + clsx + tailwind-merge + lucide-react — every dependency it needs is already installed (see tech-stack § UI primitives). It is not a runtime dependency: it generates source files into src/components/ui/ that we own, modify freely, and version with the rest of the codebase.
Initialisation is a single command:
Then components are added on demand:
bunx shadcn@latest add button card dialog dropdown-menu sheet tabs sonner skeleton form input label select separator
For a Casual+Builder two-track product, the realistic M2 component set is roughly: Button, Card, Sheet, Dialog, DropdownMenu, Tabs, Sonner (toasts), Skeleton, Form (which pulls in react-hook-form + zod — zod is already installed but unused per the audit), Input, Label, Select, Separator, Popover, Calendar (for price-history picker if we add range filtering), Combobox, DataTable (built on @tanstack/react-table).
Why shadcn/ui specifically¶
- Zero lock-in. Components live in our repo, not a
node_moduleswall. We can fork any component the moment it stops fitting. - Tailwind 4 native. No CSS-in-JS to manage; the components ship Tailwind classes that the Figma friend's tokens (set in
globals.css@theme inline) recolour immediately. - Activates the already-installed Radix packages. The 7 unused Radix packages flagged in the audit become useful the moment shadcn generates Button (uses
react-slot), Dialog, DropdownMenu, Select, Slider, Tabs, Tooltip, Checkbox. - Figma-friend workflow alignment. The Figma → shadcn path is the most well-trodden in the industry as of 2026:
- Figma component sets map 1:1 to shadcn primitives (Button variants ↔
button.tsxcva variants; Card sizes ↔ Card padding tokens; Dialog modes ↔ Dialog open/close states). v0.dev(Vercel's design-to-code tool) generates shadcn-shaped React from screenshots, Figma frames, or text prompts — it could become the friend's review-to-prototype shortcut without changing our component philosophy.- Theming is a single Tailwind config — when the friend produces a palette / radius / font scale in Figma, it lands in
globals.cssand every shadcn component picks it up. - Bootstrapping seam. New contributors run
bunx shadcn add <component>and the component is in their working tree in under 30 seconds — no hunting through a docs site for usage examples, no version-skew between the docs and the installed package. - Sane defaults that aren't AI-slop. The default shadcn theme is intentionally neutral (small radius, restrained shadows, system fonts) — designed to be themed away. Adopting shadcn doesn't lock in a generic SaaS aesthetic; it gives us a base for the Figma friend to reskin against the brand decisions in #42.
Recommendation: add Prettier + prettier-plugin-tailwindcss¶
The audit flagged "no Prettier configured" as a Gap. With shadcn coming in (and the Figma friend producing class-heavy components), auto-sorting Tailwind classes prevents bikeshedding and keeps PR diffs small.
Lands as part of the same PR as shadcn init.
Defer: Storybook¶
Storybook 8 gives the Figma friend a way to review components in isolation, browse variants, and check states without spinning up the full app. Adds friction if no one uses it; pays for itself if the friend's design review needs a "show me Button in disabled + dark mode" workflow.
Recommendation: defer. Decide once the friend's review workflow is concrete. If it turns out he wants per-component review surfaces, add Storybook in a follow-up ticket. If he reviews via the live app, we don't need it.
Defer: Bun as package manager¶
Faster installs, native TS execution (would replace tsx for the scraper runner). Friction: changes onboarding shape for any future contributor not on Bun, and package-lock.json would become bun.lockb. Re-evaluate after shadcn lands; not a blocker for this RFC.
Defer: TanStack Query¶
RSC + server actions handle most data fetching natively in Next 16. TanStack Query becomes worth it only when client-heavy interactive flows need optimistic updates, cache invalidation, infinite scroll, or background refetch — none of which are M2 commitments today. Re-evaluate when #15 cart aggregator or #14 price drop alerts start asking for it.
Trade-offs¶
| Cost | What it buys |
|---|---|
Component code lives in our repo, so we maintain it. Upstream shadcn updates are not auto-pulled — running bunx shadcn diff button to see upstream changes is a manual habit. |
We control breaking changes, can fork freely, never have to wait for a library author to merge a fix. |
Day-one installation is bunx shadcn add for each component. Not a single npm i and you have everything. |
Smaller bundle (only ship what you add); no surprise breaking changes from a v2 of the library. |
| Theming uses Tailwind config + CSS variables. The Figma friend's tokens have to be expressed in those primitives, not as a Figma plugin export. | Single source of truth (the Tailwind config) makes design ↔ code drift visible immediately. |
| Adds Prettier as a project dep. One more thing to keep aligned with editor settings. | Class-sorting consistency removes a class of PR comments; sets the foundation for the Figma friend's contributions to land cleanly. |
Doesn't ship a built-in dark-mode toggle. shadcn's dark mode is a Tailwind config + a next-themes integration we'd add ourselves. |
We control the toggle UX (and where the toggle lives), not the library. The competitive landscape doesn't show dark-mode preference data — we can ship light-only at v1 and add dark when the brand work calls for it. |
Some components require react-hook-form + zod to use idiomatically (Form). Both are mainstream; zod is already installed and unused. |
Single typed schema for client-validation + server-action validation + Prisma-shape inference — a coherent story. |
Alternatives¶
Mantine¶
Full-featured component library with built-in styling. Hooks-rich (notifications, modals, forms). Strong DataTable.
- Where it loses: uses Emotion (CSS-in-JS) for styling. Emotion + RSC has a long-standing friction story — works, but with caveats. Re-introduces a runtime cost we don't pay today. Locks in Mantine's design language unless heavily themed (which fights the library's grain).
- When it wins: if we wanted "set up component primitives in 5 minutes and never touch them again" and didn't care about shipping CSS-in-JS overhead. Not us.
Chakra UI¶
Similar shape to Mantine. Theme tokens are first-class. Accessible by default.
- Where it loses: also Emotion-based; same RSC friction. Visually has a recognisable "Chakra look" that takes effort to escape — the opposite of what the Figma friend's brand work needs.
- When it wins: small admin tools where the default look is acceptable.
MUI (Material-UI)¶
The biggest, most mature React component library. Material Design.
- Where it loses: Material Design is the AI-slop default we explicitly reject (per feedback memory + ADR-0002 rationale — the v0.1 prototype was rejected as "AI slop"). Restyling MUI to not look like Material Design is fighting the library upstream; it's never finished. Heavy bundle. Emotion-based.
- When it wins: internal-only enterprise tools where Material Design is the desired aesthetic.
Ant Design¶
Mature, opinionated, enterprise-flavoured. Strong DataTable, Form, Calendar.
- Where it loses: Visually corporate — even more so than MUI. Localisation is good for Chinese market; less so for ours. CSS-in-JS internals.
- When it wins: B2B SaaS dashboards. Not consumer-facing aggregators.
Park UI (Ark UI based)¶
Very similar shape to shadcn — generates components into your repo, framework-agnostic (Vue/Svelte/Solid as well as React), built on Ark UI primitives instead of Radix.
- Where it loses: smaller ecosystem, less mature tooling (no
v0.devanalog), Ark UI is younger than Radix. The Radix packages are already installed in our repo — picking Park UI means deleting them and switching primitives. - When it wins: non-React stacks. For us, shadcn is the strictly better fit because we're React + Radix-already-installed.
Headless UI (Tailwind Labs)¶
Tailwind's own primitives library. Lightweight.
- Where it loses: smaller component set than Radix (no Combobox, no DataTable foundation, less granular keyboard handling). shadcn doesn't use Headless UI; switching would mean rebuilding the primitive layer.
- When it wins: very minimal projects where Radix is overkill. Not us.
DaisyUI¶
Tailwind plugin that ships pre-styled components as utility classes (<button class="btn btn-primary">).
- Where it loses: locks in DaisyUI's visual language (rounded-medium, slightly playful, ~20 themes). Doesn't pair with Radix primitives — hands off all behavior to your own JS. Theming away from DaisyUI's defaults is doable but constant.
- When it wins: weekend hackathons, marketing sites where the DaisyUI look is the desired look.
"Pick nothing — keep raw Tailwind + Radix, build components ourselves"¶
The current state.
- Where it loses: every PR re-litigates "what does our Button look like?" / "what's the Dialog overlay opacity?" / "should this be a Sheet or a Dialog?" The Figma friend lands in a project with no shared vocabulary; every Figma frame becomes a translation job from scratch.
- When it wins: if we have a strong opinion that our components must be 100% bespoke, with no influence from any library's defaults. Possible — but the default brand work in #42 hasn't started, and shadcn's neutral starting point is a better blank slate than literal
<div>s with Tailwind utilities.
Open questions¶
These need MASTER input before this RFC moves to Accepted:
- Initial component set. Which components to install on day 1 vs. lazy-add as M2 features need them? Recommendation: install the M2 polish-list (Button, Card, Sheet, Dialog, DropdownMenu, Tabs, Sonner, Skeleton, Form, Input, Label, Select, Separator, Popover) on the shadcn-init PR. Lazy-add the rest. Confirm or change.
- Storybook yes / no. Decide when the Figma friend's review workflow is concrete. Probably "no" for now per the recommendation above.
- Theming flow. The Figma friend produces a palette + typography + radii. Does that come into the repo as a Tailwind config patch (PR), as a JSON file the build reads, or as a Figma export the friend shares offline? Affects how design ↔ code stays in sync.
- Form library. Recommend
react-hook-form+zod(shadcn's idiomatic pair, both ecosystem standards,zodalready installed). Confirm. - DataTable. Recommend
@tanstack/react-table(shadcn's recommended pairing, headless, performance is fine at our cardinality). Confirm or defer. - Dark mode. Defer or include? Recommend deferring until the brand work in #42 signals whether it's a brand requirement or a "nice to have." Most consumer aggregators in our space ship light-only first.
Implementation plan¶
Once MASTER accepts, the work splits into one decision-recording PR and one implementation PR:
1. Decision-recording PR (small)¶
- Lock the decision as ADR
0006(or whichever number is next): "Frontend component library is shadcn/ui." - Update tech-stack reference — promote the Radix packages from "Unused" to "Used via shadcn primitives" once init lands; note the shadcn convention in the UI primitives section.
- Update
docs/design-system.md— re-frame as "the shadcn-customisation map for our brand" rather than a from-scratch design-system doc; this aligns with #27.
2. Implementation PR (separate ticket)¶
-
bunx shadcn@latest init— generatecomponents.json,lib/utils.ts, base CSS variables inglobals.css -
bunx shadcn@latest addfor the M2 component list - Add Prettier +
prettier-plugin-tailwindcss; commit.prettierrc; runbunx prettier --write .once for an initial sort - Update
eslint.config.mjsto disable rules that conflict with Prettier (eslint-config-prettier) - Wire
react-hook-form+ reuse the already-installedzodfor the first form (Builder save flow when #12 lands) - Update Local setup guide with the
bunx shadcn add <component>workflow
This implementation work belongs on a new ticket — the audit ticket #34 closes when this RFC + ADR ship; the actual shadcn init is its own work.
Out of scope¶
- Choosing specific shadcn component variants. That's design-system work for #27 and the Figma friend.
- Building a custom design system from scratch. Explicitly the alternative this RFC rejects ("pick nothing").
v0.devadoption as a workflow standard. Mentioned as a tooling option for the Figma friend; not a commitment. Friend can use it or not.- Brand identity decisions (palette, typography, microcopy, voice). Owned by #42.
- Page-level designs. Owned by #28.
- Storybook adoption — explicitly deferred above.
- Bun migration — explicitly deferred above.
- TanStack Query adoption — explicitly deferred above.
- Mobile app shell (React Native / Expo / native). Not in M1-M3 scope.