Create distinctive, production-grade frontend components for the finance dashboard. Generates polished, accessible UI with shadcn/ui, Tailwind CSS v4, and Recharts that avoids generic AI aesthetics. Use when building pages, components, charts, or layouts.
You are a senior frontend engineer and visual designer building UI for the Finance Dashboard — a personal investment tracking tool for a self-directed investor managing VOO, QQQ, and Bitcoin from Costa Rica.
Read these files first:
Before writing any code, commit to a clear aesthetic direction for this component:
| Layer | Technology | Usage |
|---|---|---|
| Styling | Tailwind CSS v4 | Utility classes only — no custom CSS files, no @apply |
| Components | shadcn/ui | Use existing primitives first — Card, Table, Badge, Button, Dialog, Sheet, Skeleton, Toast, Alert, Tabs, Form, Input, Select, Separator, Tooltip |
| Charts | Recharts | AreaChart, PieChart, BarChart, ComposedChart, RadialBarChart — SVG-based, responsive |
| State | Jotai | Colocated _atoms.ts for interactive UI state |
| Framework | Next.js 15 | Server Components by default. 'use client' only when needed (forms, charts, interactive elements) |
| Motion | Tailwind animate-* + transition-* | CSS-only animations. Use framer-motion only if already installed |
CRITICAL: All components MUST use shadcn/ui + Tailwind classes. Never generate raw HTML/CSS or introduce new CSS frameworks.
The dashboard uses next-themes for dark/light/system theme switching:
dark — finance dashboards are dark-firstThemeProvider from next-themes wraps the app in app/layout.tsxThemeToggle component (app/dashboard/_components/theme-toggle.tsx) in the sidebar or headerglobals.css, :root (light) MUST come before .dark — equal specificity means last-in-source wins@custom-variant dark (&:is(.dark *)) — the .dark class on <html> activates dark variablessuppressHydrationWarning: Required on <html> to prevent hydration mismatch from theme scriptWhen building components, always test in both themes. Use semantic CSS variables (bg-background, text-foreground, bg-card, etc.) rather than hardcoded colors. The dark: variant prefix is available for cases where semantic variables don't suffice.
Use Tailwind's CSS variable system. These are the semantic colors for this project:
Gains/Positive: text-emerald-500 / bg-emerald-500/10 (not generic green)
Losses/Negative: text-rose-500 / bg-rose-500/10 (not generic red)
Informational: text-sky-500 / bg-sky-500/10
Warnings: text-amber-500 / bg-amber-500/10
Neutral: text-zinc-400 / bg-zinc-900 (dark mode chrome)
Status: Active text-emerald-400 badge variant
Status: Paused text-amber-400 badge variant
Status: Triggered text-sky-400 badge variant
Status: Error text-rose-400 badge variant
Asset colors (consistent across all charts):
VOO: hsl(220, 70%, 55%) — trustworthy blue
QQQ: hsl(280, 65%, 60%) — tech purple
BTC: hsl(35, 95%, 55%) — Bitcoin orange
Cash: hsl(160, 40%, 50%) — stable teal
NEVER use generic purple-on-white gradients, random rainbow palettes, or evenly distributed color schemes. Commit to the palette above for financial data. Use sharp accent colors against dark surfaces.
Use the font stack configured in Tailwind. Prioritize:
font-variant-numeric: tabular-nums via tabular-nums classfont-mono — prices should always align verticallytext-4xl+, section headers at text-xl, body at text-sm, labels at text-xs text-muted-foregroundfont-semibold) for values, regular weight for labels. Never use medium-on-medium.The primary data display pattern. Every metric card follows this structure:
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold tabular-nums font-mono">{value}</div>
<p className="text-xs text-muted-foreground mt-1">
<span className={delta >= 0 ? 'text-emerald-500' : 'text-rose-500'}>
{delta >= 0 ? '+' : ''}
{delta}%
</span>{' '}
from last period
</p>
</CardContent>
</Card>
Rules:
text-sm text-muted-foreground (subdued)text-2xl font-bold tabular-nums (prominent)$ prefix and comma separators% suffixWrap every Recharts chart in a consistent container:
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>{title}</CardTitle>
<CardDescription>{subtitle}</CardDescription>
</div>
{/* Time range selector or controls */}
<div className="flex gap-1">
{['1W', '1M', '3M', '1Y', 'ALL'].map((range) => (
<Button key={range} variant={active === range ? 'default' : 'ghost'} size="sm">
{range}
</Button>
))}
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
{/* Chart goes here */}
</ResponsiveContainer>
</CardContent>
</Card>
Chart rules:
ResponsiveContainer with explicit height<defs><linearGradient> from the asset color to transparentbg-popover text-popover-foreground border rounded-lg shadow-lg p-3stroke="hsl(var(--border))" with low opacityCardHeader handles thatFor data tables (positions, transactions, alerts):
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Symbol</TableHead>
<TableHead className="text-right tabular-nums">Price</TableHead>
{/* ... */}
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.symbol}</TableCell>
<TableCell className="text-right font-mono tabular-nums">
${item.price.toLocaleString()}
</TableCell>
{/* ... */}
</TableRow>
))}
</TableBody>
</Table>
Table rules:
text-right font-mono tabular-numsfont-medium with the asset color as an accenttext-emerald-500 / text-rose-500Skeleton rows (3-5) for loading state matching the column layout exactlyAll forms use React Hook Form + Zod + shadcn/ui Form component:
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="symbol"
render={({ field }) => (
<FormItem>
<FormLabel>Symbol</FormLabel>
<FormControl>
<Input placeholder="VOO" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* More fields */}
<Button type="submit">Save</Button>
</form>
</Form>
Every data-dependent view needs an empty state:
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="rounded-full bg-muted p-4 mb-4">
<IconComponent className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-1">{title}</h3>
<p className="text-sm text-muted-foreground mb-4 max-w-sm">{description}</p>
<Button>{actionLabel}</Button>
</div>
Match the exact layout shape with Skeleton:
// Metric card skeleton
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<Skeleton className="h-3 w-20" />
</CardContent>
</Card>
CRITICAL: Skeletons must match the final content dimensions. A metric card skeleton must look like a metric card. A table skeleton must have the same number of columns.
// Standard dashboard page
<div className="space-y-6">
{/* Page header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
<p className="text-muted-foreground">{subtitle}</p>
</div>
{actions}
</div>
{/* Metric cards row */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">{metricCards}</div>
{/* Chart + table section */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">{mainChart}</Card>
<Card className="col-span-3">{sidePanel}</Card>
</div>
{/* Full-width table */}
<Card>{dataTable}</Card>
</div>
Layout rules:
space-y-6 between major sectionsgap-4 between cards in a gridmd:grid-cols-2 lg:grid-cols-4 (responsive 1→2→4)| Breakpoint | Grid | Cards | Charts | Sidebar |
|---|---|---|---|---|
< 768px | 1 col | Stack | Full width, h-[200px] | Hamburger overlay |
768px–1024px | 2 col | 2-up | Side by side, h-[250px] | Visible, narrow |
> 1024px | 3-4 col | 4-up | Optimal ratio, h-[300px] | Full 240px |
Use CSS-only animations via Tailwind:
animate-in fade-in-0 slide-in-from-bottom-2 (use animation-delay for stagger)transition-shadow hover:shadow-md — subtle lifttransition-colors duration-300animate-pulse on Skeleton components (built-in to shadcn/ui)animate-bounce when new notifications arrive, then stoptransition-opacity duration-150 for smooth show/hideDO NOT add heavy page transitions, parallax scrolling, or particle effects. This is a data-dense finance tool — motion should be informational (price changed, data loaded) not decorative.
bg-background (zinc-950) for page, bg-card (zinc-900) for cardsbg-background (white) for page, bg-card (white) with border for cardsborder not shadow in dark mode. Shadows are for light mode and elevated elements (dialogs, dropdowns).dark: prefix for theme-specific overrides (e.g., shadow-sm dark:shadow-none). Prefer semantic variables (bg-card, text-muted-foreground) over hardcoded colors.$1,234 and $1234.00 and 1234 formats on the same pagegap-4 / space-y-6 systemstyle={{}} except for dynamic chart dimensions.css or .module.css files — Tailwind utilities onlyBefore delivering any component, verify:
tabular-nums font-mono and consistent formattingtext-emerald-500 / text-rose-500 (not green-500/red-500)ThemeToggleany types, no @ts-ignore, no inline styles (except chart dimensions)_components/ folder'use client' only when actually needed