App

Metric Card

Compact KPI card: a metric name, a large tabular value, an optional outline badge with a trend-up or trend-down icon and screen-reader label, and an optional description line.

Free, MIT licenseOpen in Browse
import type { ComponentProps, ReactNode } from "react"
import { TrendDownIcon, TrendUpIcon } from "@phosphor-icons/react/dist/ssr"

import { Badge } from "@/components/ui/badge"
import {
  Card,
  CardAction,
  CardDescription,
  CardHeader,
} from "@/components/ui/card"

type MetricCardProps = Omit<
  ComponentProps<typeof Card>,
  "title" | "children" | "size"
> & {
  /**
   * The stock `Card` size: `default` puts 24px of space around and between
   * the card's parts, `sm` 16px.
   */
  size?: ComponentProps<typeof Card>["size"]
  /** The metric's name, shown as a small muted label at the top. */
  name: ReactNode
  /** The figure itself, in large type with tabular numerals. */
  value: ReactNode
  /**
   * A change badge in the top-right corner. `value` is its visible text,
   * `trend` picks an up or down arrow, and `label` is read to screen readers
   * before the value, since the arrow is hidden from them.
   */
  change?: { value: ReactNode; trend: "up" | "down"; label: string }
  /** A line of muted text under the value. */
  description?: ReactNode
}

/**
 * A card that shows one metric on a dashboard: its name, its value in large
 * type, an optional change badge and an optional line of text below.
 *
 * It takes no children. `className` and other props land on the `Card`.
 */
function MetricCard({
  name,
  value,
  change,
  description,
  ...props
}: MetricCardProps) {
  return (
    <Card data-slot="metric-card" {...props}>
      <CardHeader>
        <CardDescription>{name}</CardDescription>
        {change && (
          <CardAction>
            <Badge variant="outline">
              {change.trend === "up" ? (
                <TrendUpIcon
                  weight="light"
                  aria-hidden="true"
                  data-icon="inline-start"
                />
              ) : (
                <TrendDownIcon
                  weight="light"
                  aria-hidden="true"
                  data-icon="inline-start"
                />
              )}
              <span className="sr-only">{change.label}</span>
              {change.value}
            </Badge>
          </CardAction>
        )}
        <p className="text-2xl font-medium tabular-nums">{value}</p>
        {description != null && (
          <CardDescription>{description}</CardDescription>
        )}
      </CardHeader>
    </Card>
  )
}

export { MetricCard }
export type { MetricCardProps }
export default MetricCard

Installation

pnpm dlx shadcn add @01orbital/metric-card
npx shadcn add @01orbital/metric-card
yarn shadcn add @01orbital/metric-card
bunx --bun shadcn add @01orbital/metric-card

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/analytics/metric-card.tsx, as source you own and edit.

Usage

import { MetricCard } from "@/components/analytics/metric-card"

// The demo's words. Change copy here, not in the markup.
const COPY = {
  name: "Total visitors",
  value: "248.5k",
  change: "12.4%",
  up: "Up",
  period: "vs last period",
} as const

export default function MetricCardDemo() {
  return (
    <>
      {/* 276px: one column of the four-up dashboard row this card is made
          for (72rem less three 16px gaps, over four). The card sets its own
          width: a centred grid item otherwise shrinks to its content. */}
      <MetricCard
        className="w-full max-w-[17.25rem]"
        name={COPY.name}
        value={COPY.value}
        change={{ value: COPY.change, trend: "up", label: COPY.up }}
        description={COPY.period}
      />
    </>
  )
}

API reference

Prop

Type

It also accepts the HTML attributes of the element it renders.

Dependencies

npm: @phosphor-icons/react. The CLI installs them for you.

Registry pieces it installs with it:

  • badge
  • card