# Key facts

Property key facts row with icons and numbers that count up when scrolled into view.

- Type: component
- Category: Data
- Tier: Free
- Dependencies: lucide-react, motion

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/key-facts.json
```

## Usage

```tsx
import { KeyFacts } from "@/components/facade/key-facts";

export default function Page() {
  return <KeyFacts />;
}
```

## Source

```tsx
"use client";

import * as React from "react";
import { animate, useInView, useReducedMotion } from "motion/react";
import { Bath, BedDouble, Calendar, DollarSign, Ruler, Trees, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export interface KeyFact {
  label: string;
  value: number;
  icon?: LucideIcon;
  prefix?: string;
  suffix?: string;
  /** Decimal places to show. */
  decimals?: number;
  /** Disable thousands separators (useful for years). */
  noGrouping?: boolean;
  /** Use compact notation, e.g. 1.25M. */
  compact?: boolean;
}

export interface KeyFactsProps {
  facts?: KeyFact[];
  /** Duration of the count-up in seconds. */
  duration?: number;
  className?: string;
}

const defaultFacts: KeyFact[] = [
  { label: "List price", value: 1250000, icon: DollarSign, prefix: "$", compact: true, decimals: 2 },
  { label: "Bedrooms", value: 4, icon: BedDouble },
  { label: "Bathrooms", value: 3.5, icon: Bath, decimals: 1 },
  { label: "Interior", value: 3120, icon: Ruler, suffix: " sqft" },
  { label: "Lot size", value: 0.34, icon: Trees, suffix: " ac", decimals: 2 },
  { label: "Year built", value: 2019, icon: Calendar, noGrouping: true },
];

type FormatOptions = Pick<KeyFact, "value" | "decimals" | "compact" | "noGrouping">;

function format(fact: FormatOptions, n: number) {
  if (fact.compact && Math.abs(fact.value) >= 1000) {
    const abs = Math.abs(fact.value);
    const div = abs >= 1_000_000 ? 1_000_000 : 1000;
    const unit = abs >= 1_000_000 ? "M" : "K";
    const scaled = n / div;
    return `${scaled.toLocaleString("en-US", {
      minimumFractionDigits: fact.decimals ?? 0,
      maximumFractionDigits: fact.decimals ?? 0,
    })}${unit}`;
  }
  return n.toLocaleString("en-US", {
    minimumFractionDigits: fact.decimals ?? 0,
    maximumFractionDigits: fact.decimals ?? 0,
    useGrouping: !fact.noGrouping,
  });
}

function CountUp({ fact, start, duration }: { fact: KeyFact; start: boolean; duration: number }) {
  const reduce = useReducedMotion();
  const ref = React.useRef<HTMLSpanElement>(null);
  const { value, decimals, compact, noGrouping } = fact;
  const from = noGrouping ? Math.round(value * 0.97) : 0;

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const opts: FormatOptions = { value, decimals, compact, noGrouping };
    if (!start) {
      el.textContent = format(opts, from);
      return;
    }
    if (reduce) {
      el.textContent = format(opts, value);
      return;
    }
    const controls = animate(from, value, {
      duration,
      ease: [0.22, 1, 0.36, 1],
      onUpdate: (v) => {
        const factor = Math.pow(10, decimals ?? 0);
        el.textContent = format(opts, compact ? v : Math.round(v * factor) / factor);
      },
    });
    return () => controls.stop();
  }, [start, reduce, value, decimals, compact, noGrouping, from, duration]);

  return (
    <span className="tabular-nums">
      {fact.prefix}
      <span ref={ref}>{format(fact, from)}</span>
      {fact.suffix && <span className="text-[0.6em] font-medium text-muted-foreground">{fact.suffix}</span>}
    </span>
  );
}

export function KeyFacts({ facts = defaultFacts, duration = 1.4, className }: KeyFactsProps) {
  const ref = React.useRef<HTMLDListElement>(null);
  const inView = useInView(ref, { once: true, margin: "-60px 0px" });

  return (
    <dl
      ref={ref}
      className={cn(
        "grid w-full grid-cols-2 overflow-hidden rounded-2xl border border-border bg-card text-card-foreground sm:grid-cols-3 lg:grid-cols-6",
        "[&>div]:border-border [&>div]:border-b [&>div]:border-r",
        className,
      )}
    >
      {facts.map((f) => {
        const Icon = f.icon;
        return (
          <div key={f.label} className="relative -mb-px -mr-px flex flex-col gap-3 p-4 sm:p-5">
            <dt className="flex items-center gap-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
              {Icon && (
                <span className="grid size-7 place-items-center rounded-lg bg-accent text-accent-foreground">
                  <Icon className="size-3.5" aria-hidden="true" />
                </span>
              )}
              {f.label}
            </dt>
            <dd className="font-display text-2xl font-semibold tracking-tight text-foreground sm:text-[1.75rem]">
              <CountUp fact={f} start={inView} duration={duration} />
            </dd>
          </div>
        );
      })}
    </dl>
  );
}

export default KeyFacts;
```
