Template · React · HTML
Business finance
A business finance dashboard in Tiles: income against payments, a growth ring against target, a stock sparkline, activity and a verification checklist.
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/business-finance.jsonnpx shadcn@latest add @manniche/business-financebusiness-finance.tsx
// A business finance screen in Tiles: income against payments, a growth ring, a stock sparkline, activity and a
// verification checklist.
import { cn } from '@/lib/utils'
import { BigNumber, DeltaPill } from '@/registry/manniche/chart-kit/chart-kit'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile, TileLegend } from '@/registry/manniche/data-tile/data-tile'
import { AreaChart, type AreaPoint } from '@/registry/manniche/area-chart/area-chart'
import { Donut } from '@/registry/manniche/donut/donut'
import { Sparkline } from '@/registry/manniche/sparkline/sparkline'
import { TransactionList, type Transaction } from '@/registry/manniche/transaction-list/transaction-list'
export type VerificationStep = { id: string; label: string; done: boolean; hint?: string }
export type BusinessFinanceProps = {
data: {
/** Income per period, with `previous` holding payments for the same period. */
cashflow: AreaPoint[]
income: number
incomeChange: number
/** Growth toward the target, 0 to 1. */
growth: number
growthTarget: string
stock: { label: string; value: number }[]
stockName: string
activity: Transaction[]
verification: VerificationStep[]
}
format?: ValueFormat
title?: string
note?: string
today?: string
/** The current time. Used for `today` when that is not given. */
now?: Date
labels?: { income?: string; payments?: string; growth?: string; /** Under the share in the growth ring. Default "of target". */ ofTarget?: string; stock?: string; activity?: string; verification?: string; stepsDone?: (done: number, total: number) => string; done?: string; todo?: string }
className?: string
}
const DEFAULT_FORMAT: ValueFormat = { currency: 'EUR', decimals: 0 }
/** Business finance: cash flow, growth ring, stock sparkline, activity and a verification checklist. */
// "YYYY-MM-DD" of `now` in local time, for TransactionList's "Today" and "Yesterday".
const dayOf = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
export function BusinessFinance({ data, format = DEFAULT_FORMAT, title = 'Business finance', note = 'Example data.', today, now, labels, className }: BusinessFinanceProps) {
const day = today ?? (now ? dayOf(now) : undefined)
const l = {
income: 'Income',
payments: 'Payments',
growth: 'Growth',
ofTarget: 'of target',
stock: 'Stock',
activity: 'Activity',
verification: 'Verification',
stepsDone: (d: number, t: number) => `${d} of ${t} steps done`,
done: 'Done',
todo: 'To do',
...labels,
}
const doneCount = data.verification.filter((s) => s.done).length
const pct = Math.round(data.growth * 100)
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">
<h1 className="text-[22px] leading-tight font-semibold tracking-tight @3xl:col-span-6 @6xl:col-span-12">{title}</h1>
<DataTile
title={`${l.income} / ${l.payments}`}
className="@3xl:col-span-6 @6xl:col-span-8"
footer={<span>{note}</span>}
action={<TileLegend items={[{ label: l.income, color: 'var(--chart-1)' }, { label: l.payments, color: 'var(--chart-3)', shape: 'dashed' }]} />}
>
<div className="flex flex-wrap items-center gap-3">
<BigNumber value={data.income} format={format} size="lg" />
<DeltaPill value={data.incomeChange} format={{ decimals: 1, suffix: '%', sign: true }} />
</div>
<AreaChart className="mt-4" data={data.cashflow} label={`${l.income} and ${l.payments}`} format={format} compare labels={{ previous: l.payments }} />
</DataTile>
<DataTile title={l.growth} inverted className="@3xl:col-span-3 @6xl:col-span-4">
{/* One segment against a whole of 100, held highlighted so the centre reads the share of the target rather
than "Total 100%". */}
<Donut
data={[{ label: l.growth, value: pct }]}
total={100}
activeIndex={0}
label={`${l.growth}, ${pct} percent of ${data.growthTarget}`}
format={{ suffix: '%' }}
labels={{ ofTotal: l.ofTarget }}
/>
</DataTile>
<DataTile title={l.stock} className="@3xl:col-span-3 @6xl:col-span-4">
<p className="text-sm font-medium text-muted-foreground">{data.stockName}</p>
<Sparkline className="mt-3" data={data.stock} label={`${l.stock}, ${data.stockName}`} height={72} />
</DataTile>
<DataTile title={l.verification} className="@3xl:col-span-3 @6xl:col-span-4" footer={<span>{l.stepsDone(doneCount, data.verification.length)}</span>}>
<ol className="space-y-3">
{data.verification.map((s) => (
<li key={s.id} className="flex items-start gap-3 text-sm">
<span
aria-hidden
className={cn('mt-0.5 grid size-5 shrink-0 place-items-center rounded-full border text-[11px]', s.done ? 'border-transparent bg-foreground text-background' : 'border-border text-transparent')}
>
✓
</span>
<span>
<span className="font-medium">{s.label}</span>
<span className="sr-only"> ({s.done ? l.done : l.todo})</span>
{s.hint && <span className="block text-xs text-muted-foreground">{s.hint}</span>}
</span>
</li>
))}
</ol>
</DataTile>
<DataTile title={l.activity} className="@3xl:col-span-6 @6xl:col-span-4">
<TransactionList data={data.activity} label={l.activity} today={day} format={{ ...format, decimals: 2 }} limit={4} />
</DataTile>
</div>
</div>
)
}
export default BusinessFinancebusiness-finance-demo.tsx
import { BusinessFinance } from '@/components/business-finance'
// Example data for the demo, not a real business. "Today" is Thursday 8 October 2026.
const months = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct']
const income = [18200, 19400, 21000, 20100, 22800, 24100, 25600]
const payments = [14100, 15200, 15900, 16800, 17100, 18400, 19000]
const data = {
cashflow: months.map((label, i) => ({ label, value: income[i], previous: payments[i] })),
income: 25600,
incomeChange: 6.2,
growth: 0.68,
growthTarget: 'Yearly target',
stockName: 'Example Holdings (fictional)',
stock: [41, 43, 42, 46, 45, 49, 48, 52, 51, 55].map((value, i) => ({ value, label: `Week ${i + 1}` })),
activity: [
{ id: 'a1', name: 'Invoice 1042, Birch & Co', category: 'Invoice', date: '2026-10-08T10:12', amount: 2400 },
{ id: 'a2', name: 'Office rent', category: 'Rent', date: '2026-10-07T07:00', amount: -1350 },
{ id: 'a3', name: 'Supplier, Harbour Print', category: 'Supplies', date: '2026-10-06T15:30', amount: -412.5, status: 'pending' as const },
{ id: 'a4', name: 'Invoice 1041, Lumen Studio', category: 'Invoice', date: '2026-10-05T09:02', amount: 1800 },
],
verification: [
{ id: 'v1', label: 'Company details', done: true },
{ id: 'v2', label: 'Owner identity', done: true },
{ id: 'v3', label: 'Business address', done: true, hint: 'Confirmed by letter' },
{ id: 'v4', label: 'Bank statement', done: false, hint: 'Upload the last three months' },
],
}
export default function BusinessFinanceDemo() {
return <BusinessFinance data={data} today="2026-10-08" />
}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/business-finance.json