Skip to content

TrendIndicator

Data Display

Signed comparisons with direction, configurable success meaning, and missing-data feedback.

Positive improvementLower-is-better improvementNeutral zeroMissing comparison
Open preview

Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.

Usage

Pass an already calculated change and a concise comparison label. The component does not derive percentages from totals. Percent adds a percent sign; number displays an absolute signed delta. Set positiveIsGood=false when lower values are desirable, such as response time. Zero is neutral and null/non-finite values display No comparison. Values use en-US formatting with up to two decimal places.

Import
import { TrendIndicator } from "@/components/ui/trend-indicator";

Shipped as editable source in src/components/ui/trend-indicator.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.

Component source

The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.

src/components/ui/trend-indicator.tsx
import { ArrowDownRight, ArrowUpRight, Minus } from "lucide-react";
import { cn } from "@dashboardpack/core/lib/utils";
export interface TrendIndicatorProps {
  value: number | null;
  label: string;
  unit?: "percent" | "number";
  positiveIsGood?: boolean;
  className?: string;
}
export function TrendIndicator({
  value,
  label,
  unit = "percent",
  positiveIsGood = true,
  className,
}: TrendIndicatorProps) {
  if (value === null || !Number.isFinite(value))
    return (
      <span className={cn("text-xs text-muted-foreground", className)}>
        {label}: No comparison
      </span>
    );
  const direction = value > 0 ? "up" : value < 0 ? "down" : "flat";
  const good = direction === "flat" ? null : value > 0 === positiveIsGood;
  const Icon =
    direction === "up"
      ? ArrowUpRight
      : direction === "down"
        ? ArrowDownRight
        : Minus;
  return (
    <span
      className={cn(
        "inline-flex items-center gap-1.5 text-xs",
        good === null
          ? "text-muted-foreground"
          : good
            ? "text-success"
            : "text-destructive",
        className,
      )}
    >
      <Icon aria-hidden className="size-3.5 shrink-0" />
      <span>
        {value > 0 ? "+" : ""}
        {new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(
          value,
        )}
        {unit === "percent" ? "%" : ""}
        <span className="sr-only">
          {" "}
          {direction === "flat"
            ? "unchanged"
            : direction === "up"
              ? "increase"
              : "decrease"}
        </span>
      </span>{" "}
      <span className="text-muted-foreground">{label}</span>
    </span>
  );
}

API reference

Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.

TrendIndicator props
PropType / DefaultDescription
valuenumber | null

Default: required

Signed delta; null or non-finite means no comparison.
labelstring

Default: required

Context such as vs. previous period.
unitpercent | number

Default: percent

Whether to append the percent symbol.
positiveIsGoodboolean

Default: true

Reverse success/destructive meaning when a lower metric is better.
classNamestring

Default: —

Extend the container with semantic theme utilities.

Build with these too