015. CSS strategy: Tailwind CSS
- Date: 2026-06-06
- Status: Proposed
- Builds on: RAG012. Frontend stack: Vite + React + TypeScript, no SSR
- Tracking issue: #115
Decision
Section titled “Decision”In the context ofthe Web UI Capability (#16), where the entire application is a single chat surface (header with health dot, scrollable message list, input bar, expandable source chunks) and where the in-progress code under #17 introduced per-component.module.cssfiles,facingthe choice of CSS strategy before the surface area grows and the cost of swapping rises,we decided forTailwind CSS as the styling layer for the Web UI, installed viatailwindcss+@tailwindcss/vite, with utility classes applied inline in JSX and no component framework on top,and neglected- Continue with bespoke CSS Modules. Each component owning its own
.module.cssfile is the status quo from #17 but does not scale: every new component adds a parallel file, the design vocabulary (colors, spacing, radii) is not shared, and the in-process pattern drifts toward a private design system without the explicit decision to build one. Rejected as a starting point. - MUI (Material UI). A mature component framework with broad coverage (date pickers, tables, dialogs). Brings a heavy opinionated theme, a large bundle (~300 KB before tree-shaking), and a Material aesthetic the product does not need. The chat widget is not a forms-heavy enterprise UI; the included component breadth is wasted.
- Chakra UI. Cleaner ergonomics than MUI, accessibility-first. Still introduces a provider + theme + emotion dependency chain for a widget that needs five components. Bundle and runtime cost are not justified at this scale.
- shadcn/ui. Excellent primitives, but it is not a library: it is a copy-paste pattern that requires Tailwind underneath and adds Radix as a transitive dependency. Adopting it as the starting choice is premature; the right framing is Tailwind first, shadcn primitives layered in if and when a specific component (combobox, popover, dialog) is actually needed. This ADR does not preclude that.
- Bootstrap (or React-Bootstrap). Mature and familiar but the aesthetic is dated for a 2026 chat product, the bundle is medium-heavy, and the design tokens are less composable than Tailwind’s. The ergonomic case for utility classes wins on a project this size.
- Continue with bespoke CSS Modules. Each component owning its own
to achieve- a shared design vocabulary (color palette, spacing scale, radii, shadows) through Tailwind’s tokens, eliminating the per-component palette duplication present in the current
.module.cssfiles, - a smaller styling surface to learn and maintain — utility classes in JSX, one
index.csswith the@tailwinddirectives, onetailwind.config.ts, - a tight bundle: Tailwind’s JIT compiler emits only the classes actually used in JSX (the current chat surface measures < 5 KB of utilities), keeping the 250 KB gzipped bundle target (RAG012, #17) comfortably in reach,
- a clean upgrade path to shadcn/ui primitives later without re-platforming, if accessible composite components are needed,
- a shared design vocabulary (color palette, spacing scale, radii, shadows) through Tailwind’s tokens, eliminating the per-component palette duplication present in the current
accepting- utility class names cluster in JSX (the “long className strings” objection); the trade-off is the loss of
.module.cssround-trips and a single source of truth for the design tokens, - a build-time dependency on Tailwind’s JIT compiler runs every time the dev server starts and on every CI build; the cost is small (sub-second on this surface) but not zero,
- that swapping to a different strategy later (CSS Modules, vanilla-extract, styled-components, MUI) would require touching every JSX element that carries class names; the migration cost is real but bounded by the small component count today.
- utility class names cluster in JSX (the “long className strings” objection); the trade-off is the loss of
Consequences
Section titled “Consequences”Bundle size
Section titled “Bundle size”Tailwind’s content scanner emits only the utility classes actually present in JSX, so the production CSS is small. The chat surface today fits in a single CSS chunk under 5 KB gzipped, well below the RAG012 ≤ 250 KB total bundle target.
Component-framework re-entry trigger
Section titled “Component-framework re-entry trigger”If a future story requires accessible composite components (combobox, popover, dialog, dropdown) that are tedious to assemble from utilities alone, layer in shadcn/ui primitives on top of Tailwind without superseding this ADR. shadcn/ui is component-by-component opt-in; adopting one component does not commit the project to the rest.
If the project later acquires a true forms-heavy or data-table-heavy surface, revisit MUI or Mantine and supersede this ADR at that time.
CSS Modules re-entry trigger
Section titled “CSS Modules re-entry trigger”If the JSX class-string clutter becomes a sustained pain point measured against actual maintenance cost (not just aesthetics), reintroduce CSS Modules for the specific components that benefit. Mixing strategies is acceptable in moderation; reverting wholesale would be a new ADR.
© 2026 Benjamin Arunski