14 KiB
Technology Stack
Project: SimpleFinanceDash — UI Polish Milestone Researched: 2026-03-11 Confidence: HIGH (stack already locked; all versions confirmed from package.json and source files)
Current Stack (Already Installed — Do Not Change)
| Technology | Version | Role |
|---|---|---|
| React | 19.2.0 | UI runtime |
| TypeScript | 5.9.3 | Type safety |
| Vite | 7.3.1 | Build tool |
| Tailwind CSS | 4.2.1 | Utility styling |
| shadcn/ui | 4.0.0 | Component library |
| radix-ui | 1.4.3 | Headless primitives (single package, not individual @radix-ui/*) |
| Recharts | 2.15.4 | Charts |
| tw-animate-css | 1.4.0 | CSS animation utilities |
| Geist Variable | 5.2.8 | Primary font (already loaded, set as --font-sans) |
| lucide-react | 0.577.0 | Icon set |
Critical context: The project is on the new shadcn v4 architecture (single radix-ui package, @theme inline in CSS, oklch color space). All recommendations below are scoped to this exact setup.
Recommended Stack — UI Polish Layer
1. CSS Variable Strategy for Pastel Theme
Technique: Replace oklch values in :root inside index.css
The current index.css uses default shadcn black/white tokens:
--background: oklch(1 0 0)→ pure white--primary: oklch(0.205 0 0)→ near-black--secondary: oklch(0.97 0 0)→ near-white gray
Replace with pastel-tinted equivalents. oklch is ideal for pastel work because the C (chroma) channel controls saturation independently of lightness. A pastel is high-L, low-C:
Soft lavender background: oklch(0.97 0.015 280)
Soft blue primary: oklch(0.55 0.12 250)
Rose secondary: oklch(0.94 0.025 350)
Amber accent: oklch(0.94 0.04 85)
Soft green: oklch(0.94 0.04 145)
Finance category color tokens (add as custom vars alongside shadcn defaults):
:root {
/* Category semantic tokens — used in chart fills and row backgrounds */
--color-income: oklch(0.88 0.08 145); /* soft green */
--color-bill: oklch(0.88 0.07 250); /* soft blue */
--color-expense: oklch(0.90 0.08 85); /* soft amber */
--color-debt: oklch(0.90 0.08 15); /* soft red/rose */
--color-saving: oklch(0.88 0.07 280); /* soft violet */
--color-investment: oklch(0.90 0.07 320); /* soft pink */
}
Expose them in the @theme inline block so Tailwind generates utility classes (bg-income, bg-bill, etc.).
Confidence: HIGH — This is how shadcn v4 + Tailwind v4 theming works. The @theme inline bridging pattern is confirmed by the existing codebase structure.
2. shadcn/ui Component Customization Strategy
Pattern: Variant extension via class-variance-authority (CVA) — already available
Do not fork shadcn component source files unless necessary. Instead:
- CSS variable changes cover 80% of polish (background, border, primary, radius).
- className overrides at the call site via
cn()cover the remaining 20%. - For components that need a recurring custom style (e.g., Card with always-pastel header), wrap in a thin local component:
PastelCard,StatCard. This avoids touchingui/card.tsx.
Radius token: The current --radius: 0.625rem is fine. For a softer look, increase to 0.75rem or 1rem.
Border softness: Replace --border: oklch(0.922 0 0) with a tinted, slightly lower-opacity token like oklch(0.88 0.02 250 / 60%) for a barely-there border that reads as "pastel spreadsheet cell divider."
Confidence: HIGH
3. Chart Customization with Recharts + shadcn ChartContainer
The codebase already has shadcn's ChartContainer / ChartTooltipContent wrapper in ui/chart.tsx. This is the correct foundation.
Current gap: ExpenseBreakdown.tsx uses raw <PieChart> without ChartContainer. It should be migrated to use ChartContainer + ChartConfig so chart colors come from CSS variables rather than hardcoded hex strings.
Pattern to follow for all charts:
const chartConfig = {
bills: { label: 'Bills', color: 'var(--color-bill)' },
expenses: { label: 'Expenses', color: 'var(--color-expense)' },
savings: { label: 'Savings', color: 'var(--color-saving)' },
investments: { label: 'Investments', color: 'var(--color-investment)' },
} satisfies ChartConfig
Then use fill="var(--color-bills)" on Recharts <Cell> elements. The ChartStyle component injects these as scoped CSS vars so dark mode works automatically.
Tooltip customization: Use <ChartTooltipContent> with a formatter prop to render currency-formatted values. The existing ChartTooltipContent already handles the indicator dot pattern — just pass formatter={(value) => formatCurrency(value, currency)}.
Recharts animation: Recharts has built-in entrance animations on charts (isAnimationActive={true} is the default). The animation duration can be tuned via animationDuration (prop on <Bar>, <Pie>, <Line>). Set to 600–800ms for a polished feel. Do NOT disable animations.
Chart types in use / recommended:
PieChart— expense breakdown (already exists, needs ChartContainer migration)BarChart— budget vs actual comparison (recommended addition for FinancialOverview)RadialBarChart— AvailableBalance progress indicator (recommended to replace plain number display)
Confidence: HIGH — Recharts 2.x API is stable and well-understood. The ChartContainer pattern is confirmed from the existing ui/chart.tsx source.
4. Animation and Transition Strategy
Primary tool: tw-animate-css (already installed)
tw-animate-css provides Tailwind v4-compatible utility classes wrapping Animate.css keyframes. Use it for:
- Page entrance animations:
animate-fade-inon dashboard sections - Dialog/sheet enters:
animate-in slide-in-from-bottom-4(shadcn already applies these viadata-[state=open]variants) - Loading skeletons:
animate-pulse(Tailwind built-in, no extra library needed)
Do NOT add Motion/Framer Motion for this milestone. Reason: the interactions needed (inline edit toggle, form submission feedback, card entrance) are all achievable with CSS transitions and tw-animate-css. Framer Motion adds ~45KB to the bundle and its React 19 compatibility had rough edges in early 2025. Revisit for a future milestone if complex drag-and-drop or physics animations are needed.
Inline edit transitions (e.g., BillsTracker InlineEditRow): Use CSS transition-all duration-150 on the toggle between display and input state. This is zero-dependency and already idiomatic in the codebase.
Recommended micro-interaction patterns:
- Input focus:
focus-visible:ring-2 focus-visible:ring-primary/40for a soft glow - Row hover:
hover:bg-muted/60 transition-colors duration-150 - Button press: shadcn Button already has
active:scale-[0.98]equivalent via its CVA variants — verify inui/button.tsx - Number updates (balance changing): CSS
@keyframescount-up is overkill; a simpletransition-opacityflash on value change via akeyprop reset is sufficient
Confidence: MEDIUM — tw-animate-css is confirmed installed. Framer Motion React 19 compatibility claim is based on training data; the recommendation to skip it is defensive but well-justified on bundle size grounds alone.
5. Typography
Current state: Geist Variable is already installed (@fontsource-variable/geist) and set as --font-sans. This is the correct font for this project.
Geist Variable characteristics: Clean, geometric, excellent for data/numbers, neutral without being cold. Used by Vercel's dashboard products. Well-suited for finance UIs.
Do NOT add a second typeface. The spreadsheet aesthetic is served by a single well-chosen sans. A display font pairing would clash with the data-dense layout.
Typography scale recommendations:
/* Add to @theme inline */
--font-mono: 'Geist Mono Variable', ui-monospace, monospace;
Install @fontsource-variable/geist-mono alongside the existing font. Use font-mono tabular-nums on all currency amounts. The existing ChartTooltipContent already does this (font-mono tabular-nums class on values) — extend this pattern to table cells showing currency.
Text hierarchy for finance:
- Card titles:
text-sm font-semibold tracking-wide text-muted-foreground uppercase— reads as a spreadsheet column header - Primary numbers (balance):
text-3xl font-bold tabular-nums - Secondary numbers (line items):
text-sm tabular-nums - Labels:
text-xs text-muted-foreground
Confidence: HIGH — Font is already installed. Geist Mono is the natural companion and from the same @fontsource-variable namespace.
6. Icon Usage (lucide-react)
Already installed at 0.577.0. lucide-react is the correct choice — it's the shadcn default and provides consistent stroke-width icons that match the component style.
Pattern for finance icons:
| Concept | Lucide Icon |
|---|---|
| Income | TrendingUp |
| Bills | Receipt |
| Variable expenses | ShoppingCart |
| Debt | CreditCard |
| Savings | PiggyBank |
| Investments | BarChart2 |
| Settings | Settings2 |
| Categories | Tag |
| Budget period | CalendarDays |
Use size={16} consistently for inline icons, size={20} for standalone icon buttons.
Confidence: HIGH
7. Layout Pattern for Dashboard
The current DashboardPage.tsx uses a flat flex flex-col gap-6 p-6 layout. For polish, the recommended evolution is:
Header area: Sticky top bar with budget selector + month navigation + key stat (available balance). Removes the selector from inline flow.
Grid approach (keep existing): The lg:grid-cols-3 + lg:col-span-2 pattern for FinancialOverview + AvailableBalance is correct. Continue using CSS Grid for two-column chart sections.
Card anatomy standard:
CardHeader: Always has a soft background gradient using two adjacent pastel tokensCardTitle: Uses the uppercase label style (see Typography section)CardContent:p-0for tables,pt-4for charts/other content
This pattern already exists in the codebase (BillsTracker, FinancialOverview headers use bg-gradient-to-r from-blue-50 to-indigo-50). The task is to make it consistent and driven by category-semantic tokens rather than hardcoded Tailwind color names.
Do NOT use Tailwind's bg-blue-50 directly. Reason: when the CSS variable values are updated for theming, hardcoded bg-blue-50 won't change. Use bg-(--color-bill)/20 syntax to reference semantic tokens.
Confidence: HIGH
Alternatives Considered
| Category | Recommended | Alternative | Why Not |
|---|---|---|---|
| Animation | tw-animate-css (installed) | Framer Motion | Bundle cost (~45KB), React 19 edge cases, overkill for this scope |
| Animation | tw-animate-css (installed) | react-spring | Same — overkill, no benefit over CSS transitions for this UI |
| Charts | Recharts (installed) | Victory / Nivo | Recharts is already embedded, ChartContainer wrapper is done, switching has zero upside |
| Charts | Recharts (installed) | D3 directly | Too low-level; maintenance cost far exceeds polish benefit |
| Fonts | Geist Variable (installed) | Inter | Geist is already there; Inter would be a downgrade in distinctiveness |
| Fonts | Single font | Font pairing | Finance dashboards read better with single-family discipline |
| Color format | oklch (current) | HSL / HEX | oklch is perceptually uniform, better for generating harmonious pastel palettes |
What NOT to Use
Do NOT install:
framer-motion— React 19 had compatibility issues in early 2025; bundle cost not justified@radix-ui/*individual packages — The project is on the newradix-uiunified package; mixing them causes version conflictsreact-select/ custom dropdown libraries — shadcn'sSelectcovers the need- A CSS-in-JS solution — Tailwind v4 + CSS variables is the correct pattern; CSS-in-JS would fight the build setup
- A color picker library — custom themes are explicitly out of scope
date-fnsordayjs— only needed if date formatting becomes complex; standardIntl.DateTimeFormatis sufficientreact-hook-form— the forms in scope (login, budget creation) are simple enough for controlled inputs; adding a form library is premature
Key Tailwind v4 Syntax Notes
Important: Tailwind v4 changed syntax. The codebase correctly uses the new forms but contributors must be aware:
| v3 syntax | v4 syntax |
|---|---|
bg-opacity-50 |
bg-black/50 or oklch(.../50%) |
text-opacity-75 |
text-foreground/75 |
bg-[--color-bill] |
bg-(--color-bill) (parentheses, not brackets) |
@apply border-border |
Same (works in v4) |
| Plugin-based config | @theme inline in CSS |
Use bg-(--color-bill)/20 to reference a CSS variable with opacity. This is the correct v4 syntax.
Confidence: HIGH — Confirmed by reading the existing index.css which uses the @theme inline pattern correctly.
Installation
No new packages are required for the core polish milestone. All tools are already installed.
Optional — only if currency formatting needs locale support beyond what exists:
# Nothing to add — Intl.NumberFormat is native
If Geist Mono is desired (recommended for tabular numbers):
cd frontend && bun add @fontsource-variable/geist-mono
Then in index.css:
@import "@fontsource-variable/geist-mono";
And in @theme inline:
--font-mono: 'Geist Mono Variable', ui-monospace, monospace;
Sources
- Confirmed from codebase:
/frontend/package.json,/frontend/src/index.css,/frontend/src/components/ui/chart.tsx - shadcn v4
@theme inline+oklchcolor pattern: confirmed from existingindex.cssstructure (HIGH confidence) - Tailwind v4 CSS variable reference syntax (
bg-(--var)): confirmed from Tailwind v4 docs in training data, consistent with codebase patterns (HIGH confidence) - Recharts ChartContainer pattern: confirmed from existing
ui/chart.tsximplementation (HIGH confidence) - tw-animate-css v4 compatibility: confirmed from import in
index.css(@import "tw-animate-css") (HIGH confidence) - Framer Motion React 19 compatibility concerns: training data (MEDIUM confidence — may have improved in later 2025 patch releases, but avoidance recommendation stands on bundle-size grounds alone)