For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Stat Cards 01

A responsive set of dashboard metrics in compact and footer layouts.

Installation

Set up the @cobalt registry first, as described in Installation, then add the block:

$ pnpm dlx shadcn@latest add @cobalt/stat-cards-01

Pass your own metrics through the stats prop. Use variant="footer" for the larger layout and optional hints.

Source

"use client";

import {
  RiArrowDownCircleFill,
  RiArrowUpCircleFill,
  RiIndeterminateCircleFill,
  RiInformationFill,
} from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Reveal } from "@/components/ui/reveal";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

export type StatTone =
  | "blue"
  | "orange"
  | "purple"
  | "pink"
  | "sky"
  | "emerald";
export interface Stat {
  icon: React.ComponentType<{
    className?: string;
    "aria-hidden"?: boolean | "true" | "false";
  }>;
  label: string;
  value: string;
  delta: string;
  deltaColor: "lime" | "rose" | "neutral";
  tone?: StatTone;
  caption?: string;
  hint?: string;
}

const tones: Record<StatTone, string> = {
  blue: "bg-[image:var(--stat-tile-blue,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
  orange:
    "bg-[image:var(--stat-tile-orange,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
  purple:
    "bg-[image:var(--stat-tile-purple,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
  pink: "bg-[image:var(--stat-tile-pink,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
  sky: "bg-[image:var(--stat-tile-sky,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
  emerald:
    "bg-[image:var(--stat-tile-emerald,linear-gradient(to_bottom,var(--primary),var(--primary)))]",
};

function StatValue({ value }: { value: string }) {
  const match = value.match(/^(.*?)(-?[\d,]+(?:\.\d+)?)([^\d]*)$/);
  const target = match ? Number(match[2].replaceAll(",", "")) : Number.NaN;
  const [display, setDisplay] = React.useState(target);
  const previous = React.useRef(target);
  React.useEffect(() => {
    const from = previous.current;
    if (!Number.isFinite(target)) {
      return;
    }
    if (
      !Number.isFinite(from) ||
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    ) {
      previous.current = target;
      setDisplay(target);
      return;
    }
    if (from === target) {
      return;
    }
    const start = performance.now();
    let frame = 0;
    const tick = (now: number) => {
      const t = Math.min(1, (now - start) / 320);
      const next = from + (target - from) * (1 - (1 - t) ** 3);
      previous.current = next;
      setDisplay(next);
      if (t < 1) {
        frame = requestAnimationFrame(tick);
      }
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [target]);
  if (!match || !Number.isFinite(display)) {
    return value;
  }
  const decimals = (match[2].split(".")[1] ?? "").length;
  const number = display.toLocaleString("en-US", {
    useGrouping: match[2].includes(","),
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  });
  return (
    <>
      {match[1]}
      {number}
      {match[3]}
    </>
  );
}

function StatHint({
  label,
  hint,
  popupClassName,
}: {
  label: string;
  hint: string;
  popupClassName?: string;
}) {
  const [open, setOpen] = React.useState(false);
  return (
    <Tooltip open={open} onOpenChange={setOpen}>
      <TooltipTrigger
        render={
          <Button
            variant="ghost-muted"
            size="icon-2xs"
            shape="pill"
            type="button"
            aria-label={`About ${label}`}
            onClick={() => setOpen((value) => !value)}
          />
        }
      >
        <RiInformationFill className="size-5" aria-hidden />
      </TooltipTrigger>
      <TooltipContent size="default" className={popupClassName}>
        {hint}
      </TooltipContent>
    </Tooltip>
  );
}

export function StatCards({
  variant = "plain",
  stats,
  count,
  columns = 4,
  className,
  popupClassName,
}: {
  variant?: "plain" | "footer";
  stats: Stat[];
  count?: number;
  columns?: 1 | 2 | 4;
  className?: string;
  popupClassName?: string;
}) {
  return (
    <TooltipProvider delay={200}>
      <div
        className={cn(
          "grid w-full gap-4",
          columns === 1
            ? "grid-cols-1"
            : variant === "footer"
              ? "grid-cols-1 sm:grid-cols-2"
              : "grid-cols-2",
          columns === 4 &&
            (variant === "footer" ? "xl:grid-cols-4" : "lg:grid-cols-4"),
          className
        )}
      >
        {stats.slice(0, count ?? stats.length).map((stat, index) => {
          const DeltaIcon =
            stat.deltaColor === "lime"
              ? RiArrowUpCircleFill
              : stat.deltaColor === "rose"
                ? RiArrowDownCircleFill
                : RiIndeterminateCircleFill;
          return variant === "plain" ? (
            <Reveal
              key={stat.label}
              index={index}
              render={
                <Card
                  radius={16}
                  variant="muted"
                  // oxlint-disable-next-line shadcn/no-restyle -- reference: 132px card, icon and text sit at 16px insets with no extra gap
                  className="min-h-[132px] min-w-0 items-start justify-between gap-0"
                />
              }
            >
              <span className="mx-4 flex items-center rounded-[8px] bg-surface-card p-1.5">
                <stat.icon
                  className="size-5 shrink-0 text-foreground"
                  aria-hidden
                />
              </span>
              <CardContent className="w-full gap-0.5 max-sm:min-h-22">
                <p className="w-full text-sm leading-5 font-medium text-muted-foreground">
                  {stat.label}
                </p>
                <div className="flex w-full flex-wrap items-center gap-2">
                  <p className="min-w-0 text-2xl leading-8.5 font-medium break-all text-foreground">
                    <StatValue value={stat.value} />
                  </p>
                  <Badge variant="label" tone={stat.deltaColor}>
                    {stat.delta}
                  </Badge>
                </div>
              </CardContent>
            </Reveal>
          ) : (
            <Reveal
              key={stat.label}
              index={index}
              render={
                <Card
                  radius={16}
                  variant="muted"
                  size="xs"
                  className="row-span-3 grid min-w-0 grid-rows-subgrid"
                />
              }
            >
              <CardHeader className="flex items-start justify-between">
                <span
                  className={cn(
                    "flex size-10 shrink-0 items-center justify-center rounded-[10px] text-(color:--stat-tile-foreground,var(--primary-foreground))",
                    tones[stat.tone ?? "blue"]
                  )}
                >
                  <stat.icon className="size-5 shrink-0" aria-hidden />
                </span>
                {stat.hint && (
                  <StatHint
                    label={stat.label}
                    hint={stat.hint}
                    popupClassName={popupClassName}
                  />
                )}
              </CardHeader>
              <CardHeader className="min-w-0">
                <CardTitle variant="secondary" className="break-words">
                  {stat.label}
                </CardTitle>
                <p className="min-w-0 text-3xl leading-11 font-medium break-all text-foreground tabular-nums">
                  <StatValue value={stat.value} />
                </p>
              </CardHeader>
              <CardContent>
                <Card variant="raised" size="xs" radius={10}>
                  <CardContent className="flex-row items-center justify-between gap-2">
                    <p className="min-w-0 text-sm leading-5 break-words text-muted-foreground">
                      {stat.caption ?? "From last month"}
                    </p>
                    <Badge
                      variant="label"
                      tone={
                        stat.deltaColor === "neutral" ? "soft" : stat.deltaColor
                      }
                      shape="pill"
                      tabular
                      // oxlint-disable-next-line shadcn/no-restyle -- reference: footer delta pill is 4px icon gap, 4px left and 8px right padding (label variant is gap-0 px-1.5)
                      className="gap-1 pe-2 ps-1"
                    >
                      <span className="flex shrink-0" aria-hidden>
                        <DeltaIcon className="size-4" />
                      </span>
                      {stat.delta}
                    </Badge>
                  </CardContent>
                </Card>
              </CardContent>
            </Reveal>
          );
        })}
      </div>
    </TooltipProvider>
  );
}