KPI tile with tinted icon disc, count-up AnimatedNumber value, and a delta pill for trends.
components/ui/stat-card/stat-card.tsx@lumioui/core@latest| value | number | KPI value — counts up with AnimatedNumber on first view. |
| label | string | Caption under the value. |
| icon? | IconName | Icon in the tinted disc. |
| tone? | StatCardTone | Tint of the icon disc. |
| format? | (n: number) => string | Format the value (e.g. currency). Default: locale-formatted round. |
| delta? | number | Signed percent change — green arrow up, red arrow down, grey flat. |
| deltaLabel? | string | Small note after the label, e.g. "vs last week". |
| className? | string |
KPI tile with tinted icon disc, count-up AnimatedNumber value, and a delta pill for trends.
import { StatCard } from "@/components/ui/stat-card/stat-card";
export function Example() {
return (
<StatCard />
);
} import * as React from "react";
import { forwardRef } from "react";
import { Text, View, type ViewProps } from "react-native";
import { cn, type ColorTokens } from "@lumioui/core";
import { AnimatedNumber } from "../animated-number";
import { Icon, type IconName } from "../icon";
export type StatCardTone =
| "primary"
| "success"
| "warning"
| "destructive"
| "neutral";
const TONES: Record<StatCardTone, { disc: string; icon: keyof ColorTokens }> = {
primary: { disc: "bg-primary/10", icon: "primary" },
success: { disc: "bg-success/15", icon: "success" },
warning: { disc: "bg-warning/20", icon: "warning" },
destructive: { disc: "bg-destructive/10", icon: "destructive" },
neutral: { disc: "bg-secondary", icon: "secondaryForeground" },
};
export interface StatCardProps extends Omit<ViewProps, "className"> {
/** Icon in the tinted disc. */
icon?: IconName;
/** Tint of the icon disc. */
tone?: StatCardTone;
/** KPI value — counts up with AnimatedNumber on first view. */
value: number;
/** Format the value (e.g. currency). Default: locale-formatted round. */
format?: (n: number) => string;
/** Caption under the value. */
label: string;
/** Signed percent change — green arrow up, red arrow down, grey flat. */
delta?: number;
/** Small note after the label, e.g. "vs last week". */
deltaLabel?: string;
className?: string;
}
/**
* StatCard — KPI tile: tinted icon disc, count-up value, delta pill.
* Drop into dashboards/profile stat grids; `flex-1` it in a flex-row pair.
*/
export const StatCard = forwardRef<React.ElementRef<typeof View>, StatCardProps>(
function StatCard(
{ icon = "zap", tone = "primary", value, format, label, delta, deltaLabel, className, ...props },
ref
) {
const t = TONES[tone];
const up = (delta ?? 0) > 0;
const flat = delta === 0;
return (
<View
ref={ref}
className={cn("gap-2.5 rounded-2xl border border-border bg-card p-4", className)}
{...props}
>
<View className="flex-row items-center justify-between">
<View className={cn("h-9 w-9 items-center justify-center rounded-full", t.disc)}>
<Icon name={icon} size="sm" color={t.icon} />
</View>
{delta !== undefined ? (
<View
className={cn(
"flex-row items-center gap-0.5 rounded-full px-1.5 py-0.5",
flat ? "bg-secondary" : up ? "bg-success/15" : "bg-destructive/10"
)}
accessibilityLabel={`${Math.abs(delta)}% ${flat ? "unchanged" : up ? "up" : "down"}`}
>
<Icon
name={flat ? "minus" : up ? "arrow-up" : "arrow-down"}
size="sm"
color={flat ? "secondaryForeground" : up ? "success" : "destructive"}
/>
<Text
className={cn(
"text-[11px] font-semibold",
flat ? "text-secondary-foreground" : up ? "text-success" : "text-destructive"
)}
>
{Math.abs(delta)}%
</Text>
</View>
) : null}
</View>
<AnimatedNumber
value={value}
format={format}
className="text-2xl font-bold tracking-tight text-foreground"
/>
<Text className="text-xs text-muted-foreground" numberOfLines={1}>
{label}
{deltaLabel ? <Text className="text-xs text-muted-foreground/70"> · {deltaLabel}</Text> : null}
</Text>
</View>
);
}
);
StatCard.displayName = "StatCard";