Mikkel Manniche
Dansk English Deutsch

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.json
npx shadcn@latest add @manniche/business-finance
business-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 BusinessFinance
business-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.

Open in a new tab

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

Dependencies

    Files

    • business-finance.tsx
    • business-finance.html

    Manniche Lab · open source

    Free tools for AI agents and web development.