Search

Jump to any page.

Data display

Stat tabs

Tabs whose triggers are figures: pick a number and the page filters to it.

Causes

  • Plan changes bill customers twiceBillingOpen
  • Card freeze needs a branch visitCardsOpen
pnpm dlx shadcn@latest add @paragon/stat-tabs
Brings in@paragon/stat@paragon/tabs@base-ui/reactcn

Durations

Figures are set like Stat's: units small beside the digits, sized to fit each tab.

registry/paragon/components/stat-tabs.tsx
"use client"

import * as React from "react"
import { useDirection } from "@base-ui/react/direction-provider"
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
import { cn } from "cn"

import { StatDelta, StatFigure } from "@/registry/paragon/components/stat"

/**
 * Tabs whose triggers are figures: pick the number and the page below filters
 * to it. Use inside `Tabs`, with `TabsContent` or your own content. The active
 * figure is a raised card that slides between tabs; the rest sit recessed.
 */
function StatTabsList({ className, children, ...props }: TabsPrimitive.List.Props) {
  const direction = React.useDeferredValue(useDirection())
  return (
    <TabsPrimitive.List
      data-slot="stat-tabs-list"
      className={cn(
        "relative isolate grid grid-cols-2 gap-1 rounded-card bg-secondary p-1 md:auto-cols-fr md:grid-flow-col md:grid-cols-none",
        className
      )}
      {...props}
    >
      {children}
      {/* Base UI measures the active tab in physical pixels, so `left` and `top` stay correct in RTL.
          It measures while rendering, so after a switch between English and Arabic it remounts
          one render later, once the new direction is laid out. */}
      <TabsPrimitive.Indicator
        key={direction}
        data-slot="stat-tabs-indicator"
        renderBeforeHydration
        className="absolute top-(--active-tab-top) left-(--active-tab-left) -z-10 h-(--active-tab-height) w-(--active-tab-width) rounded-[calc(var(--radius-card)-4px)] bg-card shadow-[0_1px_2px_color-mix(in_oklab,var(--ink),transparent_90%),0_8px_20px_-12px_color-mix(in_oklab,var(--ink),transparent_75%)] ring-1 ring-border transition-[left,top,width,height] duration-(--duration-slow) ease-paragon motion-reduce:transition-none dark:shadow-none"
      />
    </TabsPrimitive.List>
  )
}

function StatTab({
  label,
  figure,
  delta,
  good = "up",
  locale = "en-GB",
  className,
  ...props
}: Omit<TabsPrimitive.Tab.Props, "children"> & {
  label: React.ReactNode
  /** A number is formatted for `locale`. A string is set as written, with its units made small. */
  figure: number | string
  delta?: number
  good?: "up" | "down"
  locale?: string
}) {
  const text = typeof figure === "number" ? new Intl.NumberFormat(locale).format(figure) : figure
  return (
    <TabsPrimitive.Tab
      data-slot="stat-tab"
      className={cn(
        "group/stat-tab relative flex min-w-0 flex-col gap-3 rounded-[calc(var(--radius-card)-4px)] px-4 pt-3.5 pb-4 text-start outline-none [--stat-max:2.75rem] [--stat-muted:var(--muted-foreground)]",
        "transition-colors duration-(--duration-base) ease-paragon not-data-active:hover:bg-foreground/4",
        "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid",
        "disabled:pointer-events-none disabled:opacity-50",
        className
      )}
      {...props}
    >
      <span className="truncate text-sm font-medium text-muted-foreground transition-colors duration-(--duration-base) group-data-active/stat-tab:text-foreground">
        {label}
      </span>
      <span className="flex items-end justify-between gap-2">
        {/* Its own container, so the figure sizes to the width the pill leaves it. */}
        <span className="@container flex min-w-0 flex-1 leading-none font-medium tracking-[-0.045em] whitespace-nowrap text-foreground/55 tabular-nums transition-colors duration-(--duration-base) ease-paragon [font-size:var(--stat-max)] group-data-active/stat-tab:text-foreground">
          <StatFigure text={text} />
        </span>
        {delta != null ? (
          <StatDelta delta={delta} good={good} locale={locale} className="mb-0.5 h-5 shrink-0 gap-0.5 px-1.5 text-xs" />
        ) : null}
      </span>
    </TabsPrimitive.Tab>
  )
}

export { StatTabsList, StatTab }