Skip to content

ADR-0018: Frontend Library Stack ​

Last Updated: 2026-06-08 Status: Active Context: Decksmith


Context ​

Before scaffolding apps/web and packages/web-ui, all frontend library choices were validated during Session B (Phase 4.0.5) — a dedicated conversational review covering every technical aspect of the frontend stack. Decisions were made with the following constraints in mind:

  • TanStack ecosystem coherence (Start, Query, Form, Table, Virtual)
  • No server-side code in apps/web (ADR-0016)
  • packages/tokens architecture already locked (ADR-0017)
  • Future mobile app (apps/mobile) must not be blocked by web-specific choices
  • Learning context: decisions must be explainable, not just pragmatic

Current Decision ​

SSR / Routing — TanStack Start ​

Already decided in ADR-0016. Validated here: TanStack Start keeps apps/api as the sole backend, preventing the erosion of the frontend/backend boundary that Next.js Server Actions and Route Handlers would introduce over time.

Data Fetching — TanStack Query ​

  • Global default staleTime: 30_000 (30s) — overridden per query as needed
  • @tanstack/react-query-devtools in dev, auto-excluded from prod build
  • Data chain: component → packages/query hook → packages/api-client → apps/api
  • Components never call fetch directly

UI Primitives — shadcn/ui + Base UI ​

shadcn/ui is used as the component scaffolding CLI (not an npm dependency — it copies source code into packages/web-ui). Base UI (by MUI, @base-ui/react) replaces Radix UI as the underlying headless primitive layer.

Rationale: Base UI reached first-class support in shadcn/ui in January 2026. Its authors include contributors from Radix UI and Floating UI — it is effectively the next generation of those libraries. Chosen at npx shadcn create time; all subsequent npx shadcn add commands use Base UI automatically.

Styling — Tailwind v4 CSS-first ​

Documented in ADR-0017. No tailwind.config.ts — CSS-first via @import "tailwindcss" and @theme { … }. Oxide engine (Rust) detects files automatically.

Forms — TanStack Form + Zod ​

TanStack Form for consistency with the TanStack ecosystem. Zod schemas (from packages/schema) are reused directly for validation — no duplication between API DTOs and form validation.

Animation — CSS + Motion ​

  • CSS (transition, @keyframes) for micro-interactions (hover, focus, toggles)
  • Motion (motion/react) for moments clés (modals, drawers, route transitions, deck add)
  • Custom SVG components for contextual icon animations (bell, trash, heart, etc.) — animate Lucide SVG paths directly with Motion. No Lottie, no separate icon system.
  • Lottie reserved for 2–3 celebratory one-off moments (success states) if needed later.

See ADR-0017 for motion duration/easing tokens.

Icons — Lucide + Motion on SVG paths ​

Lucide is the native icon set of shadcn/ui. All generated components use Lucide — keeping one icon system across packages/web-ui ensures visual consistency. Hover animations via Motion whileHover / whileTap on SVG elements. Icons requiring contextual animation (bell, trash, settings) are built as custom React components animating individual SVG paths.

State Management ​

State typeSolution
Server state (decks, cards, collection, user)TanStack Query
Shared UI state (sidebar, search, mobile nav)Zustand
Local component state (forms, hover, scroll)useState / useReducer
Scoped feature state (deck builder)useReducer + React context, scoped to route

Rule: Zustand for state shared between unrelated components. Not for server data.

Keyboard Shortcuts — tinykeys ​

tinykeys over react-hotkeys-hook for its chord sequence support (g d → go to decks, g c → go to collection, / → open search). Framework-agnostic, minimal bundle.

Utilities — Native JS first ​

No lodash or es-toolkit added upfront. Native JS (Array.prototype methods, optional chaining, Object.groupBy, etc.) is sufficient for current needs. es-toolkit added if a pattern of reimplemented helpers emerges.

Dates — Intl native ​

Intl.DateTimeFormat and Intl.RelativeTimeFormat for formatting and relative time display. Both handle locale natively — compatible with Phase 5 i18n (react-i18next). date-fns added only if complex date arithmetic is needed.

i18n — react-i18next ​

Phase 5 implementation, but one rule applies from Phase 4: no hardcoded UI strings in components. All user-visible text uses t('key') from day one, even before locale files are populated. Prevents a painful extraction refactor later.

Toasts / Notifications — Sonner ​

shadcn/ui's recommended toast library. Single-call API (toast.success(...), toast.error(...)), stacking, accessible. Integrates with CSS vars from packages/tokens.

Large Lists — TanStack Virtual ​

Essential for collection view (potentially thousands of cards). Without virtualisation, rendering large card grids freezes the browser. TanStack Virtual integrates naturally with TanStack Query's infinite queries.

Tables — TanStack Table ​

Collection table view (sortable columns, filters, configurable density). Part of the TanStack ecosystem — consistent API with Query and Virtual.

Drag and Drop — @dnd-kit/core ​

Deck builder card reordering between sections. @dnd-kit is the modern replacement for abandoned react-beautiful-dnd. Accessible, mobile-compatible, no dependency on the native HTML5 DnD API (which has known mobile bugs).

Error Boundaries — react-error-boundary ​

Minimal library wrapping React's error boundary API. Applied to critical zones (deck builder, collection, card search) to prevent a single JS error from crashing the entire page.

PDF Generation — PDFKit (in apps/worker) ​

Proxy card printing requires pixel-precise image placement, exact millimetre margins, cut lines as vector strokes, explicit DPI, and multi-format paper support. Cards are images (Scryfall JPEG/PNG) — no font rendering needed. PDFKit provides this level of control; @react-pdf/renderer's flexbox model abstracts away the precision required for print output.

PDFKit runs in apps/worker only — never in apps/api or apps/web (existing architectural rule, CLAUDE.md).


What We Explicitly Chose NOT to Add ​

CandidateReason not added
Next.jsPushes toward Server Actions / Route Handlers — erodes apps/api as sole backend
Redux / MobXOverkill — Zustand + TanStack Query covers all state needs
react-hotkeys-hookReplaced by tinykeys (chord support, smaller)
Lodash / es-toolkitNative JS sufficient — added only on proven need
Lottie / LordiconIntroduces second icon rendering system — visual inconsistency
@react-pdf/rendererInsufficient precision for proxy card print layout
Temporal APIStill requires polyfill — Intl native is sufficient
Framer Motion (old)Superseded by Motion v12 (renamed, lighter)
Storybook for apps/webStorybook is for packages/web-ui and packages/tokens only

Trade-offs ​

Benefits:

  • TanStack ecosystem coherence (Start, Query, Form, Table, Virtual) — shared mental model, consistent APIs, shared devtools
  • shadcn/ui + Base UI gives full ownership of component code — no override battles
  • Single icon system (Lucide) — no visual inconsistency from multiple icon libraries
  • Zustand + TanStack Query separation is explicit and teachable
  • Intl native for dates keeps the bundle lean and i18n-ready

Costs:

  • TanStack Start is newer — fewer community examples than Next.js
  • Custom animated SVG components require more upfront work than Lottie/Lordicon
  • tinykeys requires learning the chord syntax
  • No date-fns means no format() convenience — slightly more verbose formatting code

Risks:

  • TanStack Form is less battle-tested than React Hook Form — may hit edge cases
    • Mitigation: Zod validation is library-agnostic; switching form libs later doesn't touch the schema layer
  • @dnd-kit has a steeper API than react-beautiful-dnd
    • Mitigation: deck builder drag-and-drop is Phase 7 — time to learn the API

Evolution History ​

2026-06-08: Initial decision ​

  • Validated during Session B (Phase 4.0.5) — full conversational review of all frontend technical aspects
  • Key reversals from initial assumptions: PDFKit over @react-pdf/renderer (precision), Zustand added (UI state crosses component boundaries), Base UI over Radix for shadcn/ui
  • TanStack Virtual and Table added (missed in initial stack definition)
  • @dnd-kit and react-error-boundary added as overlooked necessities

References ​

Built with VitePress