A table-cell metric that sits a value, improvement-aware delta chip, and micro sparkline on one shared baseline, flashing a directional wash on every update.
npx shadcn@latest add @paragon/trend-cellAlso installs: highlight-on-update, sparkline, tooltip
"use client";
import * as React from "react";
import { ArrowDownRight, ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { HighlightOnUpdate } from "@/registry/paragon/ui/highlight-on-update";
import { Sparkline } from "@/registry/paragon/ui/sparkline";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/registry/paragon/ui/tooltip";
export interface TrendCellProps
extends Omit<React.ComponentProps<"span">, "children"> {
/** Current value of the metric. */
value: number;
formatValue?: (value: number) => string;
/** Signed percent vs the prior period. Computed from `data` when omitted. */
delta?: number;
/** Is a rising delta good? Latency rising is bad: pass "down". */
positiveIs?: "up" | "down";
/** Recent history, oldest first — rendered as the micro sparkline. */
data?: number[];
/** Explains the delta comparison in the chip's tooltip. */
deltaLabel?: string;
sparkWidth?: number;
/** Explicit spark stroke. Defaults to the improvement-aware tint. */
color?: string;
/** Soft background wash on value change. */
flash?: boolean;
/** Renders without draw-in animation. */
static?: boolean;
}
/**
* A table-cell metric: value, improvement-aware delta chip, and a micro
* sparkline, all sharing one text baseline so rows scan as a single line.
* Value changes flash a directional wash (green when the move is good for
* this metric, red when bad — `positiveIs="down"` inverts for
* latency-style metrics) that retargets rather than restarts under rapid
* updates. The chip explains its comparison in a tooltip, and the spark is
* its own keyboard stop with per-point readouts. Numbers are tabular so
* live updates never shift layout.
*/
export function TrendCell({
value,
formatValue = (v) => v.toLocaleString("en-US"),
delta,
positiveIs = "up",
data,
deltaLabel = "vs prior period",
sparkWidth = 64,
color,
flash = true,
static: isStatic = false,
className,
...props
}: TrendCellProps) {
// Direction of the latest change, for the wash tint.
const previous = React.useRef(value);
const direction = React.useRef<"up" | "down" | null>(null);
if (!Object.is(previous.current, value)) {
direction.current = value >= previous.current ? "up" : "down";
previous.current = value;
}
const computedDelta =
delta ??
(data && data.length > 1 && data[0] !== 0
? ((data[data.length - 1] - data[0]) / Math.abs(data[0])) * 100
: undefined);
const deltaIsGood =
computedDelta === undefined
? null
: (computedDelta >= 0) === (positiveIs === "up");
const moveIsGood =
direction.current === null
? null
: (direction.current === "up") === (positiveIs === "up");
const sparkColor =
color ??
(deltaIsGood === null
? "var(--color-muted-foreground)"
: deltaIsGood
? "var(--color-success)"
: "var(--color-destructive)");
const valueEl = (
<span className="text-sm font-medium tabular-nums">{formatValue(value)}</span>
);
return (
<span
data-slot="trend-cell"
className={cn("inline-flex items-baseline gap-2", className)}
{...props}
>
{flash ? (
<HighlightOnUpdate
value={value}
color={
moveIsGood === null ? "neutral" : moveIsGood ? "positive" : "negative"
}
>
{valueEl}
</HighlightOnUpdate>
) : (
valueEl
)}
{computedDelta !== undefined && (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<span
tabIndex={0}
className={cn(
"inline-flex cursor-default items-center gap-0.5 rounded-full px-1.5 py-px text-[10px] font-medium tabular-nums outline-none transition-[background-color,color] duration-(--duration-quick) ease-(--ease-out) focus-visible:ring-2 focus-visible:ring-ring",
deltaIsGood
? "bg-success/12 text-success"
: "bg-destructive/12 text-destructive",
)}
aria-label={`${computedDelta >= 0 ? "Up" : "Down"} ${Math.abs(
computedDelta,
).toLocaleString("en-US", { maximumFractionDigits: 1 })}% ${deltaLabel}`}
>
{computedDelta >= 0 ? (
<ArrowUpRight className="size-2.5" aria-hidden />
) : (
<ArrowDownRight className="size-2.5" aria-hidden />
)}
{Math.abs(computedDelta).toLocaleString("en-US", {
maximumFractionDigits: 1,
})}
%
</span>
</TooltipTrigger>
<TooltipContent>{deltaLabel}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{data && data.length > 1 && (
// The spark's bottom edge sits on the shared text baseline.
<Sparkline
data={data}
width={sparkWidth}
height={16}
color={sparkColor}
fillArea={false}
endDot
formatValue={formatValue}
static={isStatic}
className="self-auto"
aria-label={`Trend of last ${data.length} readings, latest ${formatValue(
data[data.length - 1],
)}`}
/>
)}
</span>
);
}