# Stats Section

> Numbers that make a claim believable, in three decisions: several of equal weight, one to remember above the rest, or each beside what it counts.

A shadcn/ui block from the [@miseui](https://miseui.junsu4621.workers.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/miseui/stats-section).

- Page, with a live preview: https://shadcn.exchange/r/miseui/stats-section
- Screenshot: https://shadcn.exchange/shot/miseui/stats-section/light.png
- Registry item: https://miseui.junsu4621.workers.dev/r/stats-section.json
- Categories: blocks
- Listed: 2026-11-05, source last changed 2026-11-05

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/miseui/stats-section.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `utils`, `https://miseui.junsu4621.workers.dev/r/count-up.json`

## Files

### `registry/miseui/blocks/stats-section.tsx`

```tsx
import { cn } from "@/registry/miseui/lib/utils"
import { DEFAULT_COPY, ROOT_CLASS, SAMPLE_STATS, StatNumber, StatsHeading, StatsNote, resolveNote, type StatsSectionCopyProps, type StatsSectionStat } from "./stats-section-parts"
import { StatsSectionPlate } from "./stats-section-plate"
import { StatsSectionSplit, type StatsSectionSplitProps } from "./stats-section-split"

type StatsSectionVariant = "row" | "plate" | "split"

interface StatsSectionProps extends StatsSectionSplitProps {
  /** `row` several numbers of equal weight, `plate` one number to remember above the rest, `split` each number beside what it counts. Anything else is `row`. */
  variant?: StatsSectionVariant
}

function StatsSectionRow({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  stats = SAMPLE_STATS,
  note,
  locale,
  className,
  ...props
}: StatsSectionCopyProps) {
  if (stats.length === 0) return null
  return (
    <section data-slot="stats-section" data-variant="row" className={cn(ROOT_CLASS, "bg-background", className)} {...props}>
      <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-12 @2xl:gap-12 @2xl:px-8 @2xl:py-16">
        <StatsHeading headline={headline} quiet={headlineQuiet} className="max-w-2xl" />
        {/* Auto-fit so three, four or five numbers all fill the line, and two columns remain at 343 px. */}
        <dl className="m-0 grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-x-6 gap-y-8">
          {stats.map((stat) => (
            <div key={stat.id} className="flex min-w-0 flex-col-reverse justify-end gap-2 border-l pl-4">
              <dt className="text-[13px] leading-5 text-pretty text-muted-foreground">{stat.label}</dt>
              <dd className="m-0">
                <StatNumber stat={stat} locale={locale} className="text-title text-3xl @3xl:text-4xl" />
              </dd>
            </div>
          ))}
        </dl>
        <StatsNote note={resolveNote(note, stats)} />
      </div>
    </section>
  )
}

function StatsSection({ variant, description, ...props }: StatsSectionProps) {
  switch (variant) {
    case "plate":
      return <StatsSectionPlate {...props} />
    case "split":
      return <StatsSectionSplit description={description} {...props} />
    default:
      return <StatsSectionRow {...props} />
  }
}

export { StatsSection, type StatsSectionProps, type StatsSectionStat, type StatsSectionVariant }
```

### `registry/miseui/blocks/stats-section-parts.tsx`

```tsx
import type * as React from "react"
import { cn } from "@/registry/miseui/lib/utils"
import { CountUp } from "@/registry/miseui/ui/count-up"

interface StatsSectionStat {
  id: string
  value: number
  label: string
  prefix?: string
  suffix?: string
  formatOptions?: Intl.NumberFormatOptions
  /** Where the number comes from, or what it counts. Shown on `split`. */
  detail?: string
}

interface StatsSectionCopyProps extends React.ComponentProps<"section"> {
  headline?: string
  /** Continues the headline in the same size, one tone quieter. */
  headlineQuiet?: string
  /** Only numbers you can back up. An empty list renders nothing. */
  stats?: readonly StatsSectionStat[]
  /** A line under the numbers. Defaults to saying the sample figures are examples, and to nothing once you pass your own. */
  note?: string | null
  /** Leave unset to follow the viewer. A server-rendered page should pass one. */
  locale?: Intl.LocalesArgument
}

const SAMPLE_STATS: readonly StatsSectionStat[] = [
  { id: "calls", value: 1240, label: "Calls summarized this week", detail: "Counted when the summary is sent, not when the call starts." },
  { id: "owners", value: 94, suffix: "%", label: "Action items with an owner", detail: "The rest are flagged, so someone can claim them." },
  { id: "saved", value: 6.5, suffix: " h", label: "Saved per person each month", detail: "From the time people told us they spent writing notes." },
  { id: "speed", value: 2, suffix: " min", label: "From hang-up to summary", detail: "The median for a call of one hour." },
]

const SAMPLE_NOTE = "Example figures for a sample product. Replace them with your own."

const DEFAULT_COPY = {
  headline: "Less time writing it down.",
  headlineQuiet: "More time doing it.",
} as const

const ROOT_CLASS = "@container flex w-full flex-col justify-center"

/** The example note must not outlive the example numbers, or it would call a real figure made up. */
function resolveNote(note: string | null | undefined, stats: readonly StatsSectionStat[]) {
  if (note !== undefined) return note
  return stats === SAMPLE_STATS ? SAMPLE_NOTE : null
}

function StatNumber({ stat, locale, className }: { stat: StatsSectionStat; locale?: Intl.LocalesArgument; className?: string }) {
  return <CountUp value={stat.value} prefix={stat.prefix} suffix={stat.suffix} formatOptions={stat.formatOptions} locale={locale} className={className} />
}

function StatsHeading({ headline, quiet, className }: { headline: string; quiet: string; className?: string }) {
  return (
    <h2 className={cn("text-title text-3xl text-balance @2xl:text-4xl", className)}>
      {headline} <span className="text-muted-foreground">{quiet}</span>
    </h2>
  )
}

function StatsNote({ note, className }: { note: string | null; className?: string }) {
  if (!note) return null
  return <p className={cn("text-[13px] leading-5 text-pretty text-muted-foreground", className)}>{note}</p>
}

export { DEFAULT_COPY, ROOT_CLASS, SAMPLE_STATS, StatNumber, StatsHeading, StatsNote, resolveNote, type StatsSectionCopyProps, type StatsSectionStat }
```

### `registry/miseui/blocks/stats-section-plate.tsx`

```tsx
import { cn } from "@/registry/miseui/lib/utils"
import { DEFAULT_COPY, ROOT_CLASS, SAMPLE_STATS, StatNumber, StatsHeading, StatsNote, resolveNote, type StatsSectionCopyProps } from "./stats-section-parts"

/** The first number is the one people should remember, so it gets the size and the soft accent, and the others stay in one quiet list. */
function StatsSectionPlate({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  stats = SAMPLE_STATS,
  note,
  locale,
  className,
  ...props
}: StatsSectionCopyProps) {
  const [lead, ...rest] = stats
  if (!lead) return null
  return (
    <section data-slot="stats-section" data-variant="plate" className={cn(ROOT_CLASS, "bg-muted dark:bg-background", className)} {...props}>
      <div className="mx-auto flex w-full max-w-6xl flex-col gap-8 px-4 py-12 @2xl:px-8 @2xl:py-16">
        <StatsHeading headline={headline} quiet={headlineQuiet} className="max-w-2xl" />
        <div className="grid grid-cols-[minmax(0,1fr)] gap-2 @3xl:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]">
          <dl data-slot="stats-section-lead" className="m-0 flex min-w-0 flex-col-reverse justify-between gap-12 rounded-3xl bg-mise-soft p-6 @3xl:p-8 dark:border dark:bg-card">
            <dt className="text-[15px] leading-relaxed text-pretty">{lead.label}</dt>
            <dd className="m-0">
              {/* Scales with the frame: a fixed size is either timid on a desktop or too wide on a phone. */}
              <StatNumber stat={lead} locale={locale} className="text-display text-[clamp(3rem,14cqi,4.5rem)]" />
            </dd>
          </dl>
          {rest.length > 0 ? (
            <dl className="m-0 flex min-w-0 flex-col justify-center divide-y rounded-3xl border bg-background px-6 dark:bg-card">
              {rest.map((stat) => (
                <div key={stat.id} className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-6 py-6">
                  <dt className="min-w-0 text-[15px] leading-relaxed text-pretty text-muted-foreground">{stat.label}</dt>
                  <dd className="m-0">
                    <StatNumber stat={stat} locale={locale} className="text-title text-3xl" />
                  </dd>
                </div>
              ))}
            </dl>
          ) : null}
        </div>
        <StatsNote note={resolveNote(note, stats)} />
      </div>
    </section>
  )
}

export { StatsSectionPlate }
```

### `registry/miseui/blocks/stats-section-split.tsx`

```tsx
import { cn } from "@/registry/miseui/lib/utils"
import { DEFAULT_COPY, ROOT_CLASS, SAMPLE_STATS, StatNumber, StatsHeading, StatsNote, resolveNote, type StatsSectionCopyProps } from "./stats-section-parts"

interface StatsSectionSplitProps extends StatsSectionCopyProps {
  /** One or two sentences beside the numbers, on `split`. */
  description?: string
}

/** A number without context is only a claim, so each one sits beside what it counts and where it comes from. */
function StatsSectionSplit({
  headline = DEFAULT_COPY.headline,
  headlineQuiet = DEFAULT_COPY.headlineQuiet,
  description = "Every call ends with a written plan, so nobody has to take notes or ask who owns what.",
  stats = SAMPLE_STATS,
  note,
  locale,
  className,
  ...props
}: StatsSectionSplitProps) {
  if (stats.length === 0) return null
  return (
    <section data-slot="stats-section" data-variant="split" className={cn(ROOT_CLASS, "bg-background", className)} {...props}>
      <div className="mx-auto grid w-full max-w-6xl grid-cols-[minmax(0,1fr)] items-start gap-8 px-4 py-12 @2xl:px-8 @2xl:py-16 @4xl:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] @4xl:gap-16">
        <div className="flex min-w-0 flex-col gap-6">
          <StatsHeading headline={headline} quiet={headlineQuiet} className="@2xl:text-3xl" />
          <p className="max-w-md text-[15px] leading-relaxed text-pretty text-muted-foreground">{description}</p>
          <StatsNote note={resolveNote(note, stats)} />
        </div>
        <dl className="m-0 flex min-w-0 flex-col divide-y border-y">
          {stats.map((stat) => (
            <div key={stat.id} className="grid grid-cols-[minmax(0,1fr)] gap-x-6 gap-y-1 py-6 @lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
              <dt className="text-[15px] leading-relaxed text-pretty @lg:col-start-2">{stat.label}</dt>
              {/* First on screen and second in the markup: a screen reader hears what is counted before the number. */}
              <dd className="m-0 -order-1 @lg:col-start-1 @lg:row-span-2 @lg:row-start-1">
                <StatNumber stat={stat} locale={locale} className="text-title text-4xl" />
              </dd>
              {stat.detail ? <dd className="m-0 text-[13px] leading-5 text-pretty text-muted-foreground @lg:col-start-2">{stat.detail}</dd> : null}
            </div>
          ))}
        </dl>
      </div>
    </section>
  )
}

export { StatsSectionSplit, type StatsSectionSplitProps }
```
