Insight Card
A stat tile: a label, a headline value, and an optional trend pill. Pass a Number Ticker as the value to have it roll in on change.
Active users
2,48112%
Failed charges
184%
import { CreditCard, Users } from "lucide-react";
import { InsightCard } from "@/components/motion/insight-card";
export function InsightCardPreview() {
return (
<div className="grid w-full max-w-sm grid-cols-2 gap-3">
<InsightCard
label="Active users"
value="2,481"
trend={{ value: 12, direction: "up" }}
icon={<Users className="h-4 w-4" />}
/>
<InsightCard
label="Failed charges"
value="18"
trend={{ value: 4, direction: "down" }}
icon={<CreditCard className="h-4 w-4" />}
/>
</div>
);
}
// easeui.dev/components/motion/insight-card
import { ArrowDownRight, ArrowUpRight } from "lucide-react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface InsightCardTrend {
/** A percentage or plain number; the sign is implied by direction, not this value. */
value: number;
direction: "up" | "down";
}
export interface InsightCardProps {
label: string;
/** The headline number. Pass a NumberTicker for it to roll in on change. */
value: ReactNode;
trend?: InsightCardTrend;
icon?: ReactNode;
className?: string;
}
/** A stat tile: a label, a headline value, and an optional trend pill. */
export function InsightCard({ label, value, trend, icon, className }: InsightCardProps) {
return (
<div
className={cn(
"flex flex-col gap-3 rounded-2xl bg-card p-4 shadow-[0_0_0_1px_var(--border)]",
className,
)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
{icon ? (
<span aria-hidden="true" className="flex h-6 w-6 shrink-0 items-center justify-center text-muted-foreground">
{icon}
</span>
) : null}
</div>
<div className="flex items-end justify-between gap-2">
<span className="font-display text-2xl font-semibold tabular-nums text-foreground">{value}</span>
{trend ? (
<span
className={cn(
"mb-0.5 inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs font-medium",
trend.direction === "up" ? "bg-success/15 text-success" : "bg-destructive/15 text-destructive",
)}
>
{trend.direction === "up" ? (
<ArrowUpRight aria-hidden="true" className="h-3 w-3" />
) : (
<ArrowDownRight aria-hidden="true" className="h-3 w-3" />
)}
{trend.value}%
</span>
) : null}
</div>
</div>
);
}
Installation
$ bunx --bun shadcn add @easeui/insight-card
- 1
Set up the theme tokens
Do this once per project. Follow the theme setup or skip it if you already ran shadcn init.
- 2
Install the dependencies
terminal npm install clsx lucide-react tailwind-merge - 3
Add the source files
components/motion/insight-card.tsx // easeui.dev/components/motion/insight-card import { ArrowDownRight, ArrowUpRight } from "lucide-react"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; export interface InsightCardTrend { /** A percentage or plain number; the sign is implied by direction, not this value. */ value: number; direction: "up" | "down"; } export interface InsightCardProps { label: string; /** The headline number. Pass a NumberTicker for it to roll in on change. */ value: ReactNode; trend?: InsightCardTrend; icon?: ReactNode; className?: string; } /** A stat tile: a label, a headline value, and an optional trend pill. */ export function InsightCard({ label, value, trend, icon, className }: InsightCardProps) { return ( <div className={cn( "flex flex-col gap-3 rounded-2xl bg-card p-4 shadow-[0_0_0_1px_var(--border)]", className, )} > <div className="flex items-center justify-between gap-2"> <span className="text-xs font-medium text-muted-foreground">{label}</span> {icon ? ( <span aria-hidden="true" className="flex h-6 w-6 shrink-0 items-center justify-center text-muted-foreground"> {icon} </span> ) : null} </div> <div className="flex items-end justify-between gap-2"> <span className="font-display text-2xl font-semibold tabular-nums text-foreground">{value}</span> {trend ? ( <span className={cn( "mb-0.5 inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 text-xs font-medium", trend.direction === "up" ? "bg-success/15 text-success" : "bg-destructive/15 text-destructive", )} > {trend.direction === "up" ? ( <ArrowUpRight aria-hidden="true" className="h-3 w-3" /> ) : ( <ArrowDownRight aria-hidden="true" className="h-3 w-3" /> )} {trend.value}% </span> ) : null} </div> </div> ); }lib/utils.ts import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | - | - |
| value | ReactNode | - | The headline number. Pass a NumberTicker for it to roll in on change. |
| trend? | InsightCardTrend | - | - |
| icon? | any | - | - |
| className? | string | - | - |
Updated