Trend badge React chart component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React trend badge, the delta chip for KPIs, table cells and leaderboards: an arrow, a signed change and a color that says if it is good.
A read-only chip, not focusable: an arrow and a signed number ("+12.4%", "−$3,200") tinted green when the change is good and red when it is bad, gray when nothing moved. Screen readers hear the direction in words ("Up 12.4% vs. last month"), never the arrow. With animated on it counts up from zero the first time it scrolls into view, glides to every new value without the pill changing width midway, and the arrow cross-fades when the direction flips. Tap the playground's Change button to watch it. Reduced motion shows the final number at once.
Trend badge is a Pro React chart component in the React chart and stat components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add trend-badge, together with Motion presets, Number format, Tones, UIProvider and useLocale and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add trend-badgeTrend badge is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import TrendBadge from @/components/ui/trend-badge.
import { TrendBadge } from "@/components/ui/trend-badge"
export function RevenueKpi() {
return (
<div className="flex flex-col gap-1">
<span className="text-[13px] text-muted-foreground">Revenue</span>
<span className="text-2xl font-semibold tabular-nums">$128,450</span>
<TrendBadge value={12.4} label="vs. last month" locale="en-US" animated />
<TrendBadge
value={-2.5}
invertTrend
formatValue={(v) => `${v < 0 ? "−" : "+"}${Math.abs(v).toLocaleString("en-US")} days`}
/>
</div>
)
}Props
value*default12.4number | nullThe change, in format units: 12.4 is +12.4% with percent, 3200 is +$3,200 with currency. null or NaN renders emptyText in gray, without an arrow.
formatdefault"percent""percent" | "number" | "currency"How the number prints, through Intl with the locale. Values of a million and up go compact ("+$1.3M").
currencydefault"USD"stringISO 4217 code for format="currency", printed with its narrow symbol ("$", "€", "£").
decimalsdefault-1numberFraction digits (0 to 6). The default -1 is auto: 1 for percent, 0 for numbers, and 0 for currency from 100 up (2 below).
formatValue(value: number) => stringFull control over the visible text, e.g. v => `−${Math.abs(v)} days`. The sr sentence uses it too.
emptyTextdefault"–"stringShown instead of the number when value is null or NaN. Screen readers hear labels.empty.
labeldefault""ReactNodeThe comparison, like "vs. last month". Joined into the spoken sentence.
labelPositiondefault"outside""inside" | "outside"outside puts the label after the pill in muted text (the stat card look) and wraps it below on narrow widths; inside keeps it in the pill.
trenddefault"auto""auto" | "up" | "down" | "flat"auto reads the sign of value. Force a direction when the meaning does not follow the sign.
flatThresholddefault0.05numberA change smaller than this (absolute, in format units) counts as no change and gets the gray look.
invertTrenddefaultfalsebooleanDown is good: costs, churn, response time. A fall turns green and a rise red.
variantdefault"soft""soft" | "solid" | "outline" | "ghost"soft is the tinted pill, solid a filled chip for emphasis, outline a hairline border, ghost colored text with an arrow and no fill (for table cells).
sizedefault"md""sm" | "md" | "lg"Height 20 / 24 / 28px with 11 / 12 / 13px text (the outside label follows). md matches the stat card's delta pill.
radiusdefault"full""none" | "sm" | "md" | "lg" | "full"Corner shape: square, 4px, 6px, 8px or a pill (the badge's radii).
icondefault"arrow""arrow" | "trend" | "chevron" | "none"Diagonal arrows, trend lines or chevrons (a dash or a flat arrow when nothing moved). Mirrors in RTL.
showSigndefaulttruebooleanPrefix gains with a plus. A loss always shows its minus (the typographic one, as wide as the plus).
neutralTonedefaultfalsebooleanEvery direction in the neutral ink, for places where green and red would suggest a judgment.
positiveTonedefault"success""neutral" | "brand" | "success" | "warning" | "danger" | "info"The color of a good change. info suits a metric that is not good or bad, like open tickets.
negativeTonedefault"danger""neutral" | "brand" | "success" | "warning" | "danger" | "info"The color of a bad change. warning softens it for metrics that are not alarming.
positiveColordefault""stringAny CSS color for a good change, replacing positiveTone in every variant. No change stays gray and neutralTone still wins.
negativeColordefault""stringAny CSS color for a bad change, replacing negativeTone in every variant.
animateddefaultfalsebooleanCounts up from zero when first in view and glides between values. Off by default so a table of badges does not all count at once.
localedefault""stringIntl locale for the number. Empty follows the UIProvider, then the page.
labels{ up?: (text: string) => string; down?: (text: string) => string; flat?: string; empty?: string }The spoken sentence. Defaults "Up {x}", "Down {x}", "No change", "No data"; e.g. up: x => `up ${x}`.
classNamesPartial<Record<"icon" | "value" | "label" | "group", string>>Extra classes for the icon, the number, the label and the wrapper around pill and outside label.
classNamestringExtra classes for the pill, merged last. Override the tone vars here, e.g. [--tone-soft:transparent].
Parts
getTrend
functionThe direction of a change: up, down, or flat when the absolute value is under the threshold (and for 0, null or NaN). Use it to pick an icon or sort rows the way the badge does. Signature: (value: number | null | undefined, options?: { flatThreshold?: number }) => "up" | "down" | "flat".
getTrendIntent
functionWhat a direction means: up is positive unless the metric is inverted (costs, churn, response time); flat is neutral. Signature: (trend: TrendDirection, invertTrend?: boolean) => "positive" | "negative" | "neutral".
DEFAULT_TREND_BADGE_LABELS
constantThe English screen reader sentences: up(text) "Up 12.4%", down(text) "Down 12.4%", flat "No change", empty "No data". Spread it to translate. Type: Required<TrendBadgeLabels>.
TrendDirection
type"up" | "down" | "flat".
TrendIntent
type"positive" | "negative" | "neutral", which picks the tone.
Examples
KPI rows
Percent, count and a custom unit; inverted for a metric where down is good.
Variants
soft, solid, outline and ghost across up, down and no change.
Sizes and icons
20, 24 and 28px with arrows, trend lines or chevrons.
In a table
Ghost badges in a right-aligned column: color without the weight of a fill.
Currency
Narrow symbols per locale, compact from a million up.
Custom tones
info and warning for metrics that are not good or bad, brand, any CSS color, and neutralTone for running text.
Edge cases
No data (with a custom emptyText), changes too small to count, a forced direction, the label inside.
Files
- components/ui/trend-badge.tsx
FAQ
Is the React Trend badge chart component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Trend badge?
Run npx wingo-ui@latest add trend-badge. It also adds the Wingo UI items it builds on (Motion presets, Number format, Tones, UIProvider and useLocale) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/trend-badge.
See also
Used in
More React chart and stat components
Articles using this component
- Shadcn KPI Cards: Deltas, Sparklines and Goal Bars
Component guides9 min read
- React Sparkline: Table Cells, KPI Tiles, Delta Chips
Component guides9 min read