Skip to content
goodtabs UI
BrowseBrowse components

Stat row

A row of stats in one frame, divided by shared hairlines.

npx shadcn@latest add https://ui.goodtabs.co/r/stat-row.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/stat-row

Components
110
Installs
4.2k

Two stats sharing one hairline.

A row of four

Visitors
12,480
+6% vs last month
Signups
842
+11% vs last month
Median load
1.1s
-0.2s vs last month
Error rate
0.04%
No change

Cells share edges, with a mono note under each value.

Usage

import { Stat, StatRow } from "@/components/ui/stat-row"

<StatRow>
  <Stat label="Visitors" value="12,480" note="+6% vs last month" />
  <Stat label="Signups" value="842" />
</StatRow>

Props

PropTypeDefaultDescription
labelstringOn Stat. What the number measures.
valueReactNodeOn Stat. The number, already formatted.
notestringOn Stat. A short mono line under the value.

Source

stat-row/stat-row.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type StatRowProps = React.ComponentProps<"dl">;

/**
 * A row of stats in one frame, split by shared hairlines. Wraps to two columns on phones.
 * Each cell draws its right and bottom edge; the inner grid hangs 1px over the frame so the outer edges hide.
 */
export function StatRow({ className, children, ...props }: StatRowProps) {
  return (
    <div data-slot="stat-row" className={cn("w-full overflow-hidden rounded-lg border bg-background", className)}>
      <dl className="m-0 -mr-px -mb-px grid grid-cols-2 sm:auto-cols-fr sm:grid-flow-col" {...props}>
        {children}
      </dl>
    </div>
  );
}

type StatProps = Omit<React.ComponentProps<"div">, "children"> & {
  /** What the number measures. */
  label: string;
  /** The number, already formatted. */
  value: React.ReactNode;
  /** Small mono note under the value, for example a change. */
  note?: string;
};

export function Stat({ label, value, note, className, ...props }: StatProps) {
  return (
    <div data-slot="stat" className={cn("flex flex-col gap-1 border-r border-b p-4", className)} {...props}>
      <dt className="text-sm text-muted-foreground">{label}</dt>
      <dd className="m-0 text-2xl font-medium tracking-tight [overflow-wrap:anywhere] tabular-nums">{value}</dd>
      {note && <dd className="m-0 font-mono text-xs text-muted-foreground">{note}</dd>}
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/stat-row/prompt.md, or the Copy AI prompt button above.

When to use

  • A strip of headline numbers under a hero or at the top of a report
  • When separate cards would add too much chrome

When not to use

  • A single number that needs an icon and a footnote: use stat-card
  • More than six numbers, where a table scans faster

Agent notes

  • Server component. It renders a description list, so label and value stay paired for screen readers
  • Columns fit the width, at least 9rem each, and two columns on phones
  • An odd number of cells on phones leaves a blank cell with no border; prefer even counts