ADR-0011 Styling
Company standard styling — Tailwind CSS v4, with shadcn/ui components on Base UI primitives.
- Decision
- Tailwind CSS v4 + shadcn/ui on Base UIBuild the app
- Status
- Accepted
- Date
- Scope
- Company-wide default for all new apps
Connections
Context
Section titled “Context”React (ADR-0001) leaves styling open. We need one way to write styles and one starting set of accessible components, so products share design tokens, components, and review habits instead of reinventing buttons and dialogs per team.
Requirements:
- Works with streaming SSR in TanStack Start (ADR-0002) — no runtime style injection on the render path
- Fits the toolchain (ADR-0035) — Vite plugin, formatter support in Oxfmt, no Prettier side-tool
- Design tokens (color, spacing, radius, type) defined once and themeable (dark mode, per-product brand)
- Accessible primitives — focus management, keyboard support, ARIA for dialogs, menus, popovers, selects — not hand-rolled per product
Options considered
Section titled “Options considered”1. Tailwind CSS v4
Section titled “1. Tailwind CSS v4”Utility-first CSS. v4 moved configuration into CSS (@theme) and ships a Vite plugin.
Pros
- Largest ecosystem; the default target for component kits (shadcn/ui), docs, and AI coding tools
- Zero runtime: compiles to one static stylesheet — ideal for streaming SSR
@tailwindcss/vitesupports Vite 8; tokens live in CSS as variables, so theming is plain CSS- Oxfmt sorts Tailwind classes natively (
sortTailwindcss) — no Prettier plugin needed - Styles live with the markup; no naming, no dead CSS files
Cons
- Maintainer risk: Tailwind Labs cut 75% of its engineering team in January 2026 (AI-driven revenue drop); new sponsors (Vercel, Google AI Studio) followed, and v4 still ships. MIT-licensed, and its output is plain CSS — a stall would freeze us, not strand us
- Long class strings; needs
cn()and variant helpers to stay readable
2. CSS Modules
Section titled “2. CSS Modules”Scoped .module.css files, built into Vite.
Pros
- Zero dependencies, plain CSS, nothing to learn
Cons
- No shared token or utility system — every product re-invents spacing scales and conventions
- No component kit ecosystem built on it
3. Panda CSS / vanilla-extract
Section titled “3. Panda CSS / vanilla-extract”Type-safe styles in TypeScript, extracted at build time.
Pros
- Type-checked tokens and variants; zero runtime
- Both actively maintained
Cons
- Smaller ecosystems; shadcn/ui and most kits target Tailwind
- An extra build-time extraction step and its own DSL to learn
4. Runtime CSS-in-JS (styled-components, Emotion)
Section titled “4. Runtime CSS-in-JS (styled-components, Emotion)”Pros: familiar to many React developers. Cons: injects styles during render — a cost under streaming SSR; Emotion has not shipped a release since 2024-12; the ecosystem has moved to zero-runtime approaches.
5. StyleX
Section titled “5. StyleX”Meta’s atomic CSS-in-JS. Cons: still pre-1.0 (0.19), small ecosystem outside Meta.
Component layer
Section titled “Component layer”- shadcn/ui — components are copied into our repo by its CLI and styled with Tailwind; we own the code. It now builds on either Radix or Base UI primitives
- Base UI (
@base-ui/react, 1.x stable) — unstyled accessible primitives from the team behind Radix, MUI, and Floating UI; composes with arenderprop - Radix — the original shadcn/ui primitives, still maintained; more existing community blocks; composes with
asChild - React Aria Components (Adobe) — the strongest accessibility and i18n coverage, but not a shadcn/ui option
- Packaged libraries (MUI, Mantine, Chakra) — fast start, but we style around their design system instead of owning ours
Decision
Section titled “Decision”Tailwind CSS v4 for styling, and shadcn/ui on Base UI as the starting component layer, are the company standard for all new apps.
Concretely:
- Tailwind via
@tailwindcss/vite, configured in one stylesheet (src/styles/app.css):@import "tailwindcss"plus@themetokens. Notailwind.config.js - Tokens are CSS variables (shadcn/ui’s
--background,--foreground,--primary, … mapped through@theme inline). Dark mode and per-product branding swap variables, not classes - shadcn/ui with a Base UI style (
base-*incomponents.json). Components live insrc/components/ui/and are ours to edit. Primitives come from@base-ui/react— never the deprecated@base-ui-components/react - Class helpers follow shadcn/ui’s defaults:
cn()(clsx+tailwind-merge) for merging,class-variance-authority(cva) for variants. Components accept and forwardclassName - Class sorting is on in Oxfmt via the root
.oxfmtrc.json(ADR-0026):sortTailwindcsswithstylesheet: "./apps/web/src/styles/app.css"andfunctions: ["cn", "cva", "clsx"], sobun run checkenforces order - No runtime CSS-in-JS and no second styling system in the same app. Plain CSS files are fine for what utilities can’t express (third-party widget overrides, complex keyframes)
The deciding factors: zero runtime on the SSR path, the largest ecosystem, native support in our toolchain (Vite plugin, Oxfmt sorting), and a component layer we own instead of rent. Base UI over Radix for its active stable releases and cleaner render composition. We accept Tailwind Labs’ maintainer risk, mitigated by the MIT license and plain-CSS output.
Consequences
Section titled “Consequences”Easier:
- One token set and one component starting point across products — design changes are variable edits
- Accessible dialogs, menus, popovers, and selects come from Base UI instead of per-team effort
- Components are plain code in the repo: change anything without fighting a library’s API
- Formatting and class order are enforced by
bun run check, so reviews skip style nits
Harder:
- shadcn/ui components are copied, not installed: upstream fixes arrive only when we pull them with the CLI and review the diff
- Sharing components across products needs a plan — a company shadcn/ui registry is the natural next step once two products duplicate the same component
- Most shadcn/ui community blocks and examples assume Radix (
asChild); Base UI versions userenderand need adapting - Watch Tailwind’s release health; if v4 stalls, the exit is freezing the version, not a rewrite
Given up: the type-checked styles of Panda CSS / vanilla-extract, and the maximum accessibility depth of React Aria. Accepted — ecosystem reach and zero-runtime simplicity matter more for a company default; a product with heavy i18n or accessibility needs can write an exception ADR for React Aria.

