Stats blocks

Number sections for reports and status boards: large figures with labels, framed as a page section rather than a dashboard tile. There are two blocks, a weekly report and a status board. The report is built from SvButton and SvToaster, the board from SvSwitchButton and SvBadge, and every value is a raw number formatted with Intl.NumberFormat, so it reads in the visitor's locale.

Installation

Add the whole group, or one block by its name (shown under each demo):

See them first with npx @svgrid/ui try blocks/stats. No project needed.

A weekly report: four figures, each with an icon, last week's value and a trend chip. Churn and load time set lowerIsBetter, so a drop shows green. Export CSV writes the raw numbers to a Blob, downloads it through an object URL that is revoked afterwards, and confirms with a toast.

Open the live example: Stats with trends block (Blocks)

npx @svgrid/ui add stats-trends

Banded

A status board on a full-width tinted band: a heading with a health badge, then a bordered, table-like grid of six cells with a value, a label, a status dot and the change since an hour ago. Cells over their warnAbove threshold turn the dot amber and the badge counts them. The Live updates switch polls every 3 seconds. Three columns become one on phones.

Open the live example: Banded stats block (Blocks)

npx @svgrid/ui add stats-banded

Formatting real numbers in the visitor's locale

Send raw numbers from the server and format them in the component. A string like "$48.6M" built on the server is fixed to the server's locale; a number plus Intl.NumberFormat options renders as 48,6 Mio. $ for a German visitor and $48.6M for an American one.

// src/routes/report/+page.server.ts
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/db'

export const load: PageServerLoad = async () => {
  const row = await db.yearSummary(2025)
  return {
    stats: [
      { label: 'Bookings handled', value: row.bookings, format: { notation: 'compact', maximumFractionDigits: 2 } },
      { label: 'Paid out to venues', value: row.payoutsUsd, format: { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1 } },
      { label: 'Repeat customers', value: row.repeatRate, format: { style: 'percent' } },
    ] satisfies { label: string; value: number; format: Intl.NumberFormatOptions }[],
  }
}
<!-- src/routes/report/+page.svelte -->
<script lang="ts">
  let { data } = $props()

  // One formatter per stat, rebuilt only when the data changes.
  const rows = $derived(data.stats.map((s) => ({
    ...s,
    text: new Intl.NumberFormat(undefined, s.format).format(s.value),
  })))
</script>

<dl>
  {#each rows as s (s.label)}
    <div><dt>{s.label}</dt><dd>{s.text}</dd></div>
  {/each}
</dl>

A few things to keep in mind:

Headless

Each block's <script module> exports its core: createWeeklyReport() and createStatusBoard(). The core takes the raw numbers as an option, formats them and owns the export or live-polling state, with no markup. Take the banded core alone with npx @svgrid/ui add stats-banded --headless and point its poll at your API. poll receives the current cells and returns the next ones. While live is true it runs every interval milliseconds (3000 by default); the timer stops when live goes false or the component unmounts:

<script lang="ts">
  import { createStatusBoard } from './stats-banded.svelte.js'

  const board = createStatusBoard({
    poll: () => fetch('/api/status/cells').then((r) => r.json()),
    interval: 10_000,
  })
</script>

<label>
  <input type="checkbox" checked={board.live} onchange={(e) => (board.live = e.currentTarget.checked)} />
  Live updates
</label>
<button onclick={board.poll}>Refresh now</button>
<p>{board.warnings} over threshold</p>
<dl>
  {#each board.cells as c (c.id)}
    <dt>{c.label}</dt>
    <dd class:warn={c.warn}>{c.shown} ({c.diffText} vs 1 h ago)</dd>
  {/each}
</dl>

See also

Live examples

  • Stats with trends block - Weekly report figures, each with an icon, last week's value and a trend chip; metrics where lower is better flip the chip color. Export CSV downloads the raw numbers and confirms with a toast.
  • Banded stats block - Status board on a tinted band: a health SvBadge and a bordered grid of six cells with value, label, status dot and change since an hour ago. An SvSwitchButton turns on live polling.