charts
Registry
Trend Card
Metric tile with overlay lines for this period vs last, a delta chip, and a compact chart.
Preview
Preview
Gross volume
$367
+$21
$346
MonNow
Churn
28.0%
−13.0pp
41.0%
MonNow
Loading
Loading
Loading
Down tone
Churn
Churn
28.0%
−13.0pp
41.0%
MonNow
Installation
Install with the shadcn CLI. This copies source into your project.
bash
npx shadcn@latest add https://livedocs.xyz/r/trend-card.jsonNamespace (after one-time setup)
npx shadcn@latest add @livedocs/trend-cardUsage
tsx
import { dailyOverlay, overlayConfig } from "@/components/ui/chart"
import { TrendCard } from "@/components/ui/trend-card"
export function Example() {
return (
<TrendCard
title="Gross volume"
value="$48,210"
baseline="$11,640"
delta="+$940"
data={dailyOverlay}
config={overlayConfig}
/>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Metric name |
| value | string | — | Primary figure |
| delta | string | — | Change chip |
| tone | "up" | "down" | "neutral" | "up" | Delta color |
| currentKey | string | "current" | Foreground series |
| compareKey | string | "previous" | Ghost series |
Source
"use client";
import * as React from "react";
import { ArrowUpRight, Info } from "lucide-react";
import {
Line,
LineChart as RechartsLineChart,
ResponsiveContainer,
Tooltip,
XAxis,
} from "recharts";
import {
ChartContainer,
ChartTooltipContent,
colorVar,
type ChartConfig,
} from "@/components/ui/chart";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ChartSkeleton, type ChartReactionOptions } from "@/components/ui/chart-reactions";
import { cn } from "@/lib/utils";
function TrendCardRoot({
title,
value,
baseline,
delta,
tone = "up",
href,
data,
config,
currentKey = "current",
compareKey = "previous",
xDataKey = "day",
className,
isLoading,
reaction,
}: {
title: string;
value: string;
baseline?: string;
delta?: string;
tone?: "up" | "down" | "neutral";
href?: string;
data: Record<string, unknown>[];
config: ChartConfig;
currentKey?: string;
compareKey?: string;
xDataKey?: string;
className?: string;
isLoading?: boolean;
reaction?: ChartReactionOptions;
}) {
const [hovered, setHovered] = React.useState<string>();
const interaction = (key: string) => ({
onMouseEnter: () => setHovered(key),
onMouseLeave: () => setHovered(undefined),
onFocus: () => setHovered(key),
onBlur: () => setHovered(undefined),
tabIndex: 0,
"aria-label": key,
className: "transition-opacity duration-150 motion-reduce:transition-none [&_.recharts-curve]:transition-opacity [&_.recharts-curve]:duration-150 motion-reduce:[&_.recharts-curve]:transition-none",
opacity: hovered ? (hovered === key ? 1 : 0.6) : key === compareKey ? 0.65 : 1,
});
const deltaClass =
tone === "down"
? "border-destructive/30 bg-destructive/15 text-destructive"
: tone === "up"
? "border-transparent bg-secondary text-[color:var(--chart-2)]"
: "border-border text-muted-foreground";
return (
<Card className={cn("relative overflow-hidden p-5 sm:p-6", className)}>
<ChartSkeleton isLoading={isLoading}>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-1.5">
<p className="text-[15px] font-medium tracking-tight">{title}</p>
<span className="text-muted-foreground" title={title}>
<Info className="size-3.5" aria-hidden />
</span>
</div>
{href ? (
<Button variant="ghost" size="icon" className="size-7" asChild>
<a href={href} aria-label={`Open ${title}`}>
<ArrowUpRight className="size-3.5" />
</a>
</Button>
) : null}
</div>
<div className="mt-3 flex flex-wrap items-end gap-2">
<p className="mt-1 tabular-nums text-4xl font-medium tracking-tight">{value}</p>
{delta ? (
<Badge variant="outline" className={cn("mb-1", deltaClass)}>
{delta}
</Badge>
) : null}
</div>
{baseline ? (
<p className="mt-2 flex items-center gap-2 tabular-nums text-[13px] text-muted-foreground">
<span className="size-2 shrink-0 rounded-full border-2 border-muted-foreground" aria-hidden />
{baseline}
</p>
) : null}
<ChartContainer
isLoading={isLoading}
loadingVariant="bar"
reaction={reaction}
config={config}
data={data}
className="mt-4 h-24 w-full"
variant="plain"
>
<ResponsiveContainer width="100%" height="100%">
<RechartsLineChart data={data} margin={{ top: 8, right: 4, left: 4, bottom: 0 }}>
<XAxis dataKey={xDataKey} hide />
<Tooltip
cursor={false}
content={<ChartTooltipContent />}
/>
<Line
type="monotone"
dataKey={compareKey}
stroke={colorVar(compareKey)}
strokeWidth={2}
dot={false}
{...interaction(compareKey)}
activeDot={{ r: 4, ...interaction(compareKey) }}
isAnimationActive={false}
/>
<Line
type="monotone"
dataKey={currentKey}
stroke={colorVar(currentKey)}
strokeWidth={3}
dot={false}
isAnimationActive={false}
{...interaction(currentKey)}
activeDot={{ r: 0 }}
/>
</RechartsLineChart>
</ResponsiveContainer>
</ChartContainer>
<p className="mt-1 flex justify-between text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
<span>{String(data[0]?.[xDataKey] ?? "")}</span>
<span>Now</span>
</p>
</ChartSkeleton>
</Card>
);
}
export const TrendCard = Object.assign(TrendCardRoot, {});