Template · React · HTML
Wallet dashboard
A wallet dashboard in Tiles: balance, a fanned stack of three payment cards, cash flow bars, a spending donut and subscriptions.
Preview
React
Install the template with shadcn. It lands in your project as ordinary components that you edit yourself.
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.jsonnpx shadcn@latest add @manniche/wallet-dashboardwallet-dashboard.tsx
// A wallet screen in Tiles: balance, a fanned stack of payment cards, cash flow, a spending donut and subscriptions.
// Compose-only: every chart is a Manniche primitive; the screen owns layout and copy.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill, Pills } from '@/registry/manniche/chart-kit/chart-kit'
import { formatValue, type ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import { CardStack, type StackCard } from '@/registry/manniche/card-stack/card-stack'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'
export type WalletSubscription = { id: string; name: string; amount: number; renews: string }
export type WalletPeriod = { id: string; label: string; long: string; flow: BarPoint[]; spending: DonutDatum[] }
export type WalletDashboardProps = {
data: {
balance: number
change: number
cards: StackCard[]
periods: WalletPeriod[]
subscriptions: WalletSubscription[]
}
format?: ValueFormat
title?: string
note?: string
labels?: {
balance?: string
cards?: string
cashFlow?: string
spending?: string
subscriptions?: string
period?: string
perMonth?: string
renews?: (date: string) => string
monthlyTotal?: string
}
/** Reserved for date-aware content; the screen itself is static. */
now?: Date
className?: string
}
const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 2 }
/** Wallet overview: balance, card stack, cash flow, spending by category and recurring payments. */
export function WalletDashboard({ data, format = DEFAULT_FORMAT, title = 'Wallet', note = 'Example data.', labels, className }: WalletDashboardProps) {
const l = {
balance: 'Total balance',
cards: 'Cards',
cashFlow: 'Cash flow',
spending: 'Spending',
subscriptions: 'Subscriptions',
period: 'Period',
perMonth: 'a month',
renews: (d: string) => `Renews ${d}`,
monthlyTotal: 'Monthly total',
...labels,
}
const [periodId, setPeriodId] = useState(data.periods[0]?.id ?? '')
const period = data.periods.find((p) => p.id === periodId) ?? data.periods[0]
const monthly = data.subscriptions.reduce((s, x) => s + x.amount, 0)
return (
<div className={cn('@container w-full font-sans text-foreground', className)}>
<div className="grid gap-4 @3xl:grid-cols-6 @6xl:grid-cols-12">
<div className="@3xl:col-span-6 @6xl:col-span-12">
<h1 className="text-[22px] leading-tight font-semibold tracking-tight">{title}</h1>
</div>
<DataTile title={l.balance} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{note}</span>}>
<BigNumber value={data.balance} format={format} size="lg" />
<div className="mt-3">
<DeltaPill value={data.change} format={{ decimals: 1, suffix: '%', sign: true }} />
</div>
</DataTile>
<DataTile title={l.cards} className="@3xl:col-span-3 @6xl:col-span-4">
<CardStack data={data.cards} label={l.cards} format={format} />
</DataTile>
<DataTile
title={l.cashFlow}
className="@3xl:col-span-6 @6xl:col-span-4"
action={<Pills label={l.period} options={data.periods.map(({ id, label }) => ({ id, label }))} value={periodId} onChange={setPeriodId} />}
>
{period && <BarChart data={period.flow} label={`${l.cashFlow}, ${period.long}`} format={{ ...format, decimals: 0 }} />}
</DataTile>
<DataTile title={l.spending} className="@3xl:col-span-3 @6xl:col-span-6">
{period && <Donut data={period.spending} label={`${l.spending}, ${period.long}`} format={format} />}
</DataTile>
<DataTile
title={l.subscriptions}
className="@3xl:col-span-3 @6xl:col-span-6"
footer={
<span>
{l.monthlyTotal}: <strong className="tabular-nums">{formatValue(monthly, { ...format, decimals: 2 })}</strong>
</span>
}
>
{/* The tile's heading names the list; a hidden <li> as its label would be counted as an extra item. */}
<ul aria-label={l.subscriptions} className="divide-y divide-border">
{data.subscriptions.map((s) => (
<li key={s.id} className="flex items-center gap-3 py-3 text-sm">
<span aria-hidden className="grid size-9 shrink-0 place-items-center rounded-full bg-muted text-xs font-semibold text-muted-foreground">
{s.name.slice(0, 2).toUpperCase()}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{s.name}</span>
<span className="block text-xs text-muted-foreground">{l.renews(s.renews)}</span>
</span>
<span className="font-medium tabular-nums">
{formatValue(s.amount, { ...format, decimals: 2 })}
<span className="sr-only"> {l.perMonth}</span>
</span>
</li>
))}
</ul>
</DataTile>
</div>
</div>
)
}
export default WalletDashboardwallet-dashboard-demo.tsx
import { WalletDashboard } from '@/components/wallet-dashboard'
// Example data for the demo, not real accounts.
const data = {
balance: 8318.06,
change: 3.2,
cards: [
{ id: 'everyday', name: 'Everyday', kind: 'Debit', ending: '4821', amount: 2091.2 },
{ id: 'travel', name: 'Travel', kind: 'Multi-currency', ending: '0937', amount: 612.08 },
{ id: 'savings', name: 'Savings', kind: 'Pot', ending: '1162', amount: 5614.78 },
],
periods: [
{
id: 'week',
label: 'Week',
long: 'last 7 days',
flow: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((label, i) => ({ label, value: [120, 340, -80, 210, 560, -150, 90][i] })),
spending: [
{ label: 'Groceries', value: 90.84 },
{ label: 'Transport', value: 47.1 },
{ label: 'Eating out', value: 74.02 },
{ label: 'Bills', value: 84.12 },
],
},
{
id: 'month',
label: 'Month',
long: 'last 30 days',
flow: ['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4'].map((label, i) => ({ label, value: [820, 640, 1180, 910][i] })),
spending: [
{ label: 'Groceries', value: 414.96 },
{ label: 'Transport', value: 179.82 },
{ label: 'Eating out', value: 235.14 },
{ label: 'Shopping', value: 152.15 },
{ label: 'Bills', value: 401.13 },
],
},
],
subscriptions: [
{ id: 'music', name: 'Northwind Music', amount: 10.99, renews: '12 Oct' },
{ id: 'cloud', name: 'Harbour Cloud', amount: 2.99, renews: '15 Oct' },
{ id: 'news', name: 'Green Leaf Daily', amount: 7.5, renews: '21 Oct' },
],
}
export default function WalletDashboardDemo() {
return <WalletDashboard data={data} />
}HTML
The whole page as one file with all CSS built in and no JavaScript. Toggles, tabs and filters work with HTML and CSS alone.
The colour in your own app
If you already have shadcn set up, these variables give the same colour. Light in :root, dark in .dark.
:root {
--primary: oklch(0.52 0.2 262);
--primary-foreground: oklch(0.99 0 0);
--accent: oklch(0.95 0.034 262);
--accent-foreground: oklch(0.36 0.140 262);
--ring: oklch(0.52 0.2 262);
}
.dark {
--primary: oklch(0.74 0.150 262);
--primary-foreground: oklch(0.18 0.024 262);
--accent: oklch(0.29 0.050 262);
--accent-foreground: oklch(0.9 0.050 262);
--ring: oklch(0.74 0.150 262);
}Install
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/wallet-dashboard.json