# Architecture Patterns: Design System
**Domain:** shadcn/ui + Tailwind CSS 4 design system for personal finance dashboard
**Researched:** 2026-03-11
**Confidence:** HIGH (based on direct codebase inspection + framework documentation patterns)
---
## Recommended Architecture
The goal is a pastel design system layered on top of shadcn/ui without replacing it. The architecture has three tiers:
1. **Token layer** — CSS custom properties in `index.css` (already exists, needs pastel values)
2. **Variant layer** — CVA-based component variants in `components/ui/` (shadcn files, lightly patched)
3. **Composition layer** — Feature components in `components/` that assemble ui primitives with domain semantics
```
frontend/src/
index.css ← Token layer: ALL CSS variables live here
lib/
utils.ts ← cn() helper (already exists)
components/
ui/ ← Variant layer: shadcn primitives (owned, patchable)
button.tsx
card.tsx
badge.tsx
...
category-badge.tsx ← Composition layer: domain-specific wrapper
stat-card.tsx ← Composition layer: reusable financial card pattern
progress-bar.tsx ← Composition layer: budget vs actual bar
page-header.tsx ← Composition layer: consistent page headers
pages/ ← Page layer: route views that compose components
```
---
## Component Boundaries
| Component | Responsibility | Communicates With |
|-----------|---------------|-------------------|
| `index.css` | All CSS variables and `@theme inline` mappings | Tailwind engine only |
| `components/ui/*` | Headless + styled primitives (shadcn-owned) | Tailwind classes, CVA variants |
| `components/*.tsx` | Domain-aware compositions (app-owned) | ui primitives, hooks, i18n |
| `pages/*.tsx` | Route-level views | components, hooks, router |
| `hooks/*.ts` | Data fetching + state | API client only |
The key boundary: `components/ui/` components must not import domain types from `lib/api.ts`. Only `components/*.tsx` and `pages/*.tsx` know about API shapes.
---
## Data Flow
```
CSS Variables (index.css)
↓ consumed by
Tailwind @theme inline → utility classes (bg-primary, text-muted-foreground, etc.)
↓ applied in
shadcn ui primitives (button, card, badge, input)
↓ assembled into
Domain components (stat-card, category-badge, progress-bar)
↓ composed into
Pages (DashboardPage, LoginPage, CategoriesPage, SettingsPage)
↑ data from
Hooks (useAuth, useBudgets) → API client (lib/api.ts) → Go REST API
```
Styling flows **downward** (tokens → primitives → components → pages). Data flows **upward** (API → hooks → pages → components via props).
---
## Patterns to Follow
### Pattern 1: Token-First Color System
**What:** Define all palette values as CSS custom properties in `:root`, then map them to Tailwind via `@theme inline`. Never write raw color values (`oklch(0.88 0.06 310)`) in component files.
**When:** Any time a new color is needed for the pastel palette.
**How it works in this codebase:**
The project uses Tailwind 4 which reads tokens exclusively from `@theme inline {}` in index.css. The two-step pattern already present in the codebase is correct — add raw values to `:root {}`, then expose them as Tailwind utilities in `@theme inline {}`.
```css
/* Step 1: Define semantic tokens in :root */
:root {
/* Pastel palette — raw values */
--pastel-pink: oklch(0.92 0.04 340);
--pastel-blue: oklch(0.92 0.04 220);
--pastel-green: oklch(0.92 0.04 145);
--pastel-amber: oklch(0.92 0.06 80);
--pastel-violet: oklch(0.92 0.04 290);
--pastel-sky: oklch(0.94 0.04 215);
/* Semantic role tokens — reference palette */
--color-income: var(--pastel-green);
--color-bill: var(--pastel-blue);
--color-expense: var(--pastel-amber);
--color-debt: var(--pastel-pink);
--color-saving: var(--pastel-violet);
--color-investment: var(--pastel-sky);
/* Override shadcn semantic tokens with pastel values */
--primary: oklch(0.55 0.14 255); /* soft indigo */
--background: oklch(0.985 0.005 240); /* barely-blue white */
--card: oklch(1 0 0);
--muted: oklch(0.96 0.01 240);
}
/* Step 2: Expose as Tailwind utilities inside @theme inline */
@theme inline {
/* Existing shadcn bridge (keep as-is) */
--color-primary: var(--primary);
--color-background: var(--background);
/* ... */
/* New pastel category utilities */
--color-income: var(--color-income);
--color-bill: var(--color-bill);
--color-expense: var(--color-expense);
--color-debt: var(--color-debt);
--color-saving: var(--color-saving);
--color-investment: var(--color-investment);
}
```
Components then use `bg-income`, `text-bill`, `bg-saving/30` etc. as Tailwind classes.
### Pattern 2: Category Color Mapping via CVA
**What:** A single `categoryVariants` CVA definition maps category types to their pastel color classes. Any component that needs category coloring imports this one function.
**When:** CategoryBadge, table rows in FinancialOverview, chart color arrays, category icons.
```typescript
// components/category-badge.tsx
import { cva } from "class-variance-authority"
export const categoryVariants = cva(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
{
variants: {
type: {
income: "bg-income/20 text-income-foreground",
bill: "bg-bill/20 text-bill-foreground",
variable_expense:"bg-expense/20 text-expense-foreground",
debt: "bg-debt/20 text-debt-foreground",
saving: "bg-saving/20 text-saving-foreground",
investment: "bg-investment/20 text-investment-foreground",
},
},
}
)
```
This is the single source of truth for category-to-color mapping. Chart colors derive from the same CSS variables (not hardcoded hex).
### Pattern 3: Wrapper Components Over shadcn Modification
**What:** When domain semantics need to be expressed (e.g., a "stat card" showing budget vs. actual), create a wrapper component in `components/` that uses shadcn Card internally. Do not modify `components/ui/card.tsx` for domain logic.
**When:** Any component that appears more than twice with the same structure across pages.
```typescript
// components/stat-card.tsx — app-owned, domain-aware
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { cn } from "@/lib/utils"
interface StatCardProps {
title: string
value: string
subtext?: string
accent?: "income" | "bill" | "expense" | "debt" | "saving" | "investment"
}
export function StatCard({ title, value, subtext, accent }: StatCardProps) {
return (
{title}
{value}
{subtext &&
{subtext}
}
)
}
```
### Pattern 4: Extend shadcn Primitives via className Override Only
**What:** When a shadcn component needs a slight visual adjustment, pass a `className` prop using `cn()` to override. Do not fork the component unless adding a new CVA variant that belongs at the primitive level.
**When:** One-off adjustments in page/feature components.
**When to actually patch `components/ui/`:** Adding a CVA variant used in 3+ places (e.g., adding a `pastel` button variant). Keep patches minimal and document them.
```typescript
// Good — className override in consumer
// Good — new CVA variant if used everywhere
// In button.tsx, add to variants.variant:
pastel: "bg-primary/15 text-primary hover:bg-primary/25 border-primary/20",
```
### Pattern 5: Recharts Color Consistency
**What:** Recharts color arrays must reference the same CSS variables used in Tailwind classes. Use `getComputedStyle` to read the variable at render time, not hardcoded hex values.
**When:** Any chart component (donut, bar, pie in ExpenseBreakdown, FinancialOverview).
```typescript
// In chart component or a lib/colors.ts utility
function getCategoryColor(type: CategoryType): string {
return getComputedStyle(document.documentElement)
.getPropertyValue(`--color-${type}`)
.trim()
}
```
---
## Anti-Patterns to Avoid
### Anti-Pattern 1: Inline Color Values in Components
**What:** Writing `bg-sky-50`, `bg-emerald-50`, `bg-amber-50` directly in component files (already present in `FinancialOverview.tsx`).
**Why bad:** The color assignment is scattered across every component. Changing a category color requires hunting every file. The existing code in `FinancialOverview.tsx` rows array hardcodes `color: 'bg-sky-50'` — this must be replaced by the category variant system.
**Instead:** All category-to-color mappings live in `categoryVariants` (CVA, single file). Components only pass the category type.
### Anti-Pattern 2: Separate CSS Files per Component
**What:** Creating `StatCard.css`, `Dashboard.css` etc.
**Why bad:** The project uses Tailwind 4's `@theme inline` — all custom styles belong in `index.css` or as Tailwind utilities. Split CSS files fragment the token system.
**Instead:** All tokens in `index.css`. All component-specific styles as Tailwind classes or CVA variants.
### Anti-Pattern 3: Dark Mode Parallel Pastel Palette
**What:** Trying to create a full pastel dark mode in the same milestone.
**Why bad:** High complexity for low value (budgeting is primarily a desktop daytime activity). The PROJECT.md explicitly defers custom themes.
**Instead:** Keep `.dark {}` block in index.css as-is (it's already defined). Focus dark mode on the existing neutral dark values. Pastel = light mode only for this milestone.
### Anti-Pattern 4: Tailwind `tailwind.config.js` for Custom Tokens
**What:** Creating a `tailwind.config.js` or `tailwind.config.ts` to add custom colors.
**Why bad:** This project uses Tailwind CSS 4 (`@import "tailwindcss"` in index.css, `@tailwindcss/vite` plugin). Tailwind 4 configures everything through CSS — `tailwind.config.js` is a Tailwind 3 pattern. Using both causes conflicts and confusion.
**Instead:** All custom tokens go in `@theme inline {}` inside `index.css`.
---
## CSS Variable Organization
The existing `index.css` should be organized into clearly labeled sections:
```css
/* 1. FRAMEWORK IMPORTS */
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";
/* 2. CUSTOM DARK VARIANT */
@custom-variant dark (&:is(.dark *));
/* 3. LIGHT MODE TOKENS */
:root {
/* === PASTEL PALETTE (raw values) === */
--pastel-pink: ...;
--pastel-blue: ...;
/* ... */
/* === CATEGORY SEMANTIC TOKENS === */
--color-income: var(--pastel-green);
/* ... */
/* === SHADCN SEMANTIC TOKENS (override defaults) === */
--background: ...;
--primary: ...;
/* ... all existing shadcn vars ... */
/* === CHART TOKENS === */
--chart-1: ...;
/* ... */
}
/* 4. DARK MODE OVERRIDES */
.dark {
/* shadcn dark vars only — no dark pastel */
}
/* 5. TAILWIND BRIDGE (@theme inline) */
@theme inline {
/* font */
--font-sans: 'Geist Variable', sans-serif;
/* shadcn token bridge (existing) */
--color-background: var(--background);
--color-primary: var(--primary);
/* ... */
/* category token bridge (new) */
--color-income: var(--color-income);
--color-bill: var(--color-bill);
/* ... */
/* radius scale (existing) */
--radius-sm: calc(var(--radius) * 0.6);
/* ... */
}
/* 6. BASE STYLES */
@layer base {
* { @apply border-border outline-ring/50; }
body { @apply bg-background text-foreground; }
html { @apply font-sans; }
}
```
---
## File Structure: Design System
```
frontend/src/
index.css ← SINGLE SOURCE: all tokens, @theme inline, base
lib/
utils.ts ← cn() (keep as-is)
colors.ts ← NEW: getCategoryColor() for Recharts, CATEGORY_TYPES map
components/
ui/ ← shadcn primitives (patchable with CVA variants)
button.tsx ← Add: pastel variant
badge.tsx ← Add: category type variants via CVA
card.tsx ← Keep as-is (className override is sufficient)
input.tsx ← May need focus ring color patch
...
category-badge.tsx ← NEW: domain badge using badge.tsx + categoryVariants
stat-card.tsx ← NEW: financial metric card
progress-bar.tsx ← NEW: budget vs actual with color coding
page-header.tsx ← NEW: consistent page header with title + actions slot
empty-state.tsx ← NEW: consistent empty state for lists
```
---
## Scalability Considerations
| Concern | Current scope | Future (theming milestone) |
|---------|--------------|---------------------------|
| Color tokens | Pastel light mode only | Add `data-theme="ocean"` etc. on ``, swap `:root` vars |
| Dark mode | Neutral dark (existing) | Pastel dark values in `.dark {}` |
| Component variants | Per-category CVA | Per-theme variant maps |
| Chart colors | CSS variable lookup | Same — already theme-aware |
The CSS variable architecture supports future theming without structural change. When custom themes are added, only `:root` values change; components need zero modification.
---
## Build Order: Foundation to Pages
This is the correct sequence because each layer depends on the previous:
**Phase A: Token foundation**
1. Define pastel palette in `index.css` `:root` (raw oklch values)
2. Define category semantic tokens in `index.css`
3. Add category tokens to `@theme inline` bridge
4. Define pastel overrides for shadcn semantic tokens (background, primary, etc.)
**Phase B: Primitive polish**
5. Patch `button.tsx` — add `pastel` variant, adjust default hover states
6. Patch `badge.tsx` — add category type variants
7. Patch `input.tsx` — ensure focus ring uses `--ring` pastel value
8. Create `lib/colors.ts` — getCategoryColor() and category type constants
**Phase C: Domain components**
9. Create `category-badge.tsx` — wraps badge with category semantics
10. Create `stat-card.tsx` — financial metric display card
11. Create `progress-bar.tsx` — budget vs actual visual
12. Create `page-header.tsx` — consistent header slot
13. Create `empty-state.tsx` — consistent empty list state
**Phase D: Page-by-page polish**
14. Login + Register pages — full branded treatment (no generic shadcn defaults)
15. Dashboard — replace hardcoded color strings with category variants, add stat cards
16. Categories page — category badge integration, polished CRUD UI
17. Settings page — form layout, preference toggles
**Phase E: Charts**
18. ExpenseBreakdown — donut chart with CSS variable colors
19. FinancialOverview chart — bar chart with category palette
20. AvailableBalance — progress indicator
Build order rationale: Pages cannot be polished until domain components exist. Domain components cannot use consistent tokens until the token layer is established. Charts come last because they depend on both the token system and the data components being stable.
---
## Sources
- Direct inspection of `frontend/src/index.css` — confirms Tailwind 4 (`@import "tailwindcss"`, `@theme inline`) and oklch color space already in use
- Direct inspection of `frontend/components.json` — confirms shadcn/ui radix-nova style, `cssVariables: true`, Tailwind 4 CSS-only config (no `tailwind.config.ts`)
- Direct inspection of `frontend/src/components/ui/button.tsx` — confirms CVA pattern with `class-variance-authority` already in use
- Direct inspection of `frontend/src/components/FinancialOverview.tsx` — identifies hardcoded color anti-pattern (`bg-sky-50`, `bg-emerald-50`) that needs migration
- `.planning/codebase/STACK.md` — confirms Tailwind CSS 4.2.1, shadcn/ui 4.0.0, CVA 0.7.1
- `.planning/PROJECT.md` — confirms pastel spreadsheet aesthetic goal, CSS variable customization approach, desktop-first target