Skip to content

RFC-0004: Frontend stack and component library

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:

bunx shadcn@latest init

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 + zodzod 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

  1. Zero lock-in. Components live in our repo, not a node_modules wall. We can fork any component the moment it stops fitting.
  2. 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.
  3. 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.
  4. Figma-friend workflow alignment. The Figma → shadcn path is the most well-trodden in the industry as of 2026:
  5. Figma component sets map 1:1 to shadcn primitives (Button variants ↔ button.tsx cva variants; Card sizes ↔ Card padding tokens; Dialog modes ↔ Dialog open/close states).
  6. 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.
  7. Theming is a single Tailwind config — when the friend produces a palette / radius / font scale in Figma, it lands in globals.css and every shadcn component picks it up.
  8. 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.
  9. 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.

// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindFunctions": ["cn", "cva"]
}

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.dev analog), 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:

  1. 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.
  2. Storybook yes / no. Decide when the Figma friend's review workflow is concrete. Probably "no" for now per the recommendation above.
  3. 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.
  4. Form library. Recommend react-hook-form + zod (shadcn's idiomatic pair, both ecosystem standards, zod already installed). Confirm.
  5. DataTable. Recommend @tanstack/react-table (shadcn's recommended pairing, headless, performance is fine at our cardinality). Confirm or defer.
  6. 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 — generate components.json, lib/utils.ts, base CSS variables in globals.css
  • bunx shadcn@latest add for the M2 component list
  • Add Prettier + prettier-plugin-tailwindcss; commit .prettierrc; run bunx prettier --write . once for an initial sort
  • Update eslint.config.mjs to disable rules that conflict with Prettier (eslint-config-prettier)
  • Wire react-hook-form + reuse the already-installed zod for 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.dev adoption 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.