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-tabsBrings 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 }