# Trend badge

> 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.

- Type: React chart component (Charts & Stats)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add trend-badge`
- Docs: https://wingo-ui.com/components/trend-badge

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add trend-badge
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/trend-badge` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/trend-badge.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | `12.4` | The 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. |
| `format?` | `"percent" \| "number" \| "currency"` | `"percent"` | How the number prints, through Intl with the locale. Values of a million and up go compact ("+$1.3M"). |
| `currency?` | `string` | `"USD"` | ISO 4217 code for format="currency", printed with its narrow symbol ("$", "€", "£"). |
| `decimals?` | `number` | `-1` | Fraction 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) => string` |  | Full control over the visible text, e.g. v => `−${Math.abs(v)} days`. The sr sentence uses it too. |
| `emptyText?` | `string` | `"–"` | Shown instead of the number when value is null or NaN. Screen readers hear labels.empty. |
| `label?` | `ReactNode` | `""` | The comparison, like "vs. last month". Joined into the spoken sentence. |
| `labelPosition?` | `"inside" \| "outside"` | `"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. |
| `trend?` | `"auto" \| "up" \| "down" \| "flat"` | `"auto"` | auto reads the sign of value. Force a direction when the meaning does not follow the sign. |
| `flatThreshold?` | `number` | `0.05` | A change smaller than this (absolute, in format units) counts as no change and gets the gray look. |
| `invertTrend?` | `boolean` | `false` | Down is good: costs, churn, response time. A fall turns green and a rise red. |
| `variant?` | `"soft" \| "solid" \| "outline" \| "ghost"` | `"soft"` | 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). |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Height 20 / 24 / 28px with 11 / 12 / 13px text (the outside label follows). md matches the stat card's delta pill. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"full"` | Corner shape: square, 4px, 6px, 8px or a pill (the badge's radii). |
| `icon?` | `"arrow" \| "trend" \| "chevron" \| "none"` | `"arrow"` | Diagonal arrows, trend lines or chevrons (a dash or a flat arrow when nothing moved). Mirrors in RTL. |
| `showSign?` | `boolean` | `true` | Prefix gains with a plus. A loss always shows its minus (the typographic one, as wide as the plus). |
| `neutralTone?` | `boolean` | `false` | Every direction in the neutral ink, for places where green and red would suggest a judgment. |
| `positiveTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"success"` | The color of a good change. info suits a metric that is not good or bad, like open tickets. |
| `negativeTone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"danger"` | The color of a bad change. warning softens it for metrics that are not alarming. |
| `positiveColor?` | `string` | `""` | Any CSS color for a good change, replacing positiveTone in every variant. No change stays gray and neutralTone still wins. |
| `negativeColor?` | `string` | `""` | Any CSS color for a bad change, replacing negativeTone in every variant. |
| `animated?` | `boolean` | `false` | Counts 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. |
| `locale?` | `string` | `""` | Intl 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}`. |
| `classNames?` | `Partial<Record<"icon" \| "value" \| "label" \| "group", string>>` |  | Extra classes for the icon, the number, the label and the wrapper around pill and outside label. |
| `className?` | `string` |  | Extra classes for the pill, merged last. Override the tone vars here, e.g. [--tone-soft:transparent]. |

## Parts

### getTrend (function)

The 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 (function)

What 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 (constant)

The 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.

## See also

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Animated Counter](https://wingo-ui.com/components/animated-counter) ([Markdown](https://wingo-ui.com/components/animated-counter.md))
- [Chart kit](https://wingo-ui.com/components/chart-kit) ([Markdown](https://wingo-ui.com/components/chart-kit.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))

---

Canonical: https://wingo-ui.com/components/trend-badge
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
