Climate

Carbon Counter

A CO2 saved-or-emitted counter that counts up to its value on view, with a fixed unit and semantic tint.

Install

npx shadcn@latest add @paragon/carbon-counter

Also installs: number-ticker

carbon-counter.tsx

"use client";

import * as React from "react";
import { Leaf, Factory } from "lucide-react";
import { NumberTicker } from "@/registry/paragon/ui/number-ticker";
import { cn } from "@/lib/utils";

export interface CarbonCounterProps extends React.ComponentProps<"div"> {
  /** Amount of CO2, in the given unit. */
  value?: number;
  /** Unit rendered after the number. */
  unit?: string;
  /** "saved" tints success and shows a leaf; "emitted" tints warning. */
  variant?: "saved" | "emitted";
  /** Caption under the readout. */
  label?: string;
  /** Small secondary line, e.g. equivalence. */
  caption?: string;
  /** Renders the target immediately, no count-up. */
  static?: boolean;
}

/**
 * A CO2 counter that counts up to its value on first view, built on the
 * number-ticker spring so the digits settle in tabular figures without layout
 * shift. The unit is separated from the animated number so it never jitters.
 * A saved / emitted variant swaps the icon and semantic tint. Reduced motion
 * (or `static`) shows the final total immediately.
 */
export function CarbonCounter({
  value = 1284.6,
  unit = "kg",
  variant = "saved",
  label = "CO₂ avoided this quarter",
  caption,
  static: isStatic = false,
  className,
  ...props
}: CarbonCounterProps) {
  const saved = variant === "saved";
  const Icon = saved ? Leaf : Factory;

  return (
    <div
      data-slot="carbon-counter"
      className={cn(
        "flex w-full max-w-xs flex-col gap-3 rounded-xl bg-card p-6 shadow-border",
        className,
      )}
      {...props}
    >
      <span
        className={cn(
          "flex size-9 items-center justify-center rounded-lg",
          saved ? "bg-success/10 text-success" : "bg-warning/15 text-warning",
        )}
      >
        <Icon className="size-5" />
      </span>

      <div className="flex items-baseline gap-1.5">
        <NumberTicker
          value={value}
          static={isStatic}
          className="text-4xl font-semibold tracking-tight"
        />
        <span className="text-lg font-medium text-muted-foreground">
          {unit}
        </span>
      </div>

      <div>
        <div className="text-sm font-medium">{label}</div>
        {caption && (
          <p className="mt-0.5 text-xs text-muted-foreground">{caption}</p>
        )}
      </div>
    </div>
  );
}