# Stats 01

> Metrics band with four large numbers that count up on scroll, labels, optional deltas, and a two-column header on large screens. Ships composable parts.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/stats-01).

- Page, with a live preview: https://shadcn.exchange/r/hirael/stats-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/stats-01-light-4794a118faa02190.png
- Registry item: https://hirael.com/r/stats-01.json
- Categories: blocks, stats
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/stats-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `https://hirael.com/r/animated-number.json`

## Files

### `registry/hirael/bases/radix/blocks/stats-01/stats-01.tsx`

```tsx
// Stats 01 from Hirael <https://hirael.com/blocks/stats/stats-01>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import { ArrowDownRight, ArrowUpRight } from 'lucide-react';

import { cn } from '@/lib/utils';
import { AnimatedNumber } from '@/registry/hirael/bases/radix/components/animated-number';

type StatsProps = React.ComponentProps<'section'>;

const Stats = ({ className, children, ...props }: StatsProps) => {
  return (
    <section data-slot="stats" className={cn('bg-background py-20 sm:py-28', className)} {...props}>
      <div className="mx-auto grid w-full max-w-[1480px] grid-cols-1 gap-12 px-4 lg:grid-cols-12 lg:items-center lg:gap-16">
        {children}
      </div>
    </section>
  );
};

type StatsHeaderProps = React.ComponentProps<'div'>;

const StatsHeader = ({ className, ...props }: StatsHeaderProps) => {
  return <div data-slot="stats-header" className={cn('flex flex-col gap-5 lg:col-span-5', className)} {...props} />;
};

type StatsEyebrowProps = React.ComponentProps<'span'>;

const StatsEyebrow = ({ className, ...props }: StatsEyebrowProps) => {
  return (
    <span data-slot="stats-eyebrow" className={cn('text-xs text-muted-foreground uppercase', className)} {...props} />
  );
};

type StatsTitleProps = React.ComponentProps<'h2'>;

const StatsTitle = ({ className, ...props }: StatsTitleProps) => {
  return (
    <h2
      data-slot="stats-title"
      className={cn(
        'font-serif text-4xl leading-[1.04] font-medium tracking-tight text-foreground sm:text-5xl',
        className,
      )}
      {...props}
    />
  );
};

type StatsDescriptionProps = React.ComponentProps<'p'>;

const StatsDescription = ({ className, ...props }: StatsDescriptionProps) => {
  return (
    <p
      data-slot="stats-description"
      className={cn('max-w-md text-sm text-muted-foreground sm:text-base', className)}
      {...props}
    />
  );
};

type StatsGridProps = React.ComponentProps<'div'>;

const StatsGrid = ({ className, ...props }: StatsGridProps) => {
  return (
    <div
      data-slot="stats-grid"
      className={cn('grid grid-cols-2 border-y border-border md:grid-cols-4 lg:col-span-7', className)}
      {...props}
    />
  );
};

type StatsItemProps = React.ComponentProps<'div'>;

const StatsItem = ({ className, ...props }: StatsItemProps) => {
  return (
    <div
      data-slot="stats-item"
      className={cn(
        'flex flex-col gap-2 border-border px-4 py-6 sm:px-5 sm:py-8',
        'md:border-s md:first:border-s-0',
        'max-md:odd:border-e max-md:[&:nth-child(n+3)]:border-t',
        className,
      )}
      {...props}
    />
  );
};

// Counts up only when the value scrolls in from off screen, so the server
// render and anything visible on load show the real number.
const useCountUp = <T extends Element>(margin = '0px 0px -10% 0px') => {
  const ref = React.useRef<T | null>(null);
  const [countUp, setCountUp] = React.useState(false);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let first = true;
    const observer = new IntersectionObserver(
      (entries) => {
        const visible = entries.some((entry) => entry.isIntersecting);
        if (first) {
          first = false;
          if (visible) observer.disconnect();

          return;
        }
        if (visible) {
          setCountUp(true);
          observer.disconnect();
        }
      },
      { rootMargin: margin },
    );
    observer.observe(el);

    return () => observer.disconnect();
  }, [margin]);

  return [ref, countUp] as const;
};

interface StatsValueProps extends Omit<React.ComponentProps<'div'>, 'children'> {
  value: number;
  prefix?: string;
  suffix?: string;
  decimals?: number;
  /** Tween length in milliseconds when the item scrolls into view. */
  duration?: number;
  format?: Intl.NumberFormatOptions;
}

const StatsValue = ({
  value,
  prefix,
  suffix,
  decimals = 0,
  duration = 1200,
  format,
  className,
  ...props
}: StatsValueProps) => {
  const [ref, countUp] = useCountUp<HTMLDivElement>();

  return (
    <div
      ref={ref}
      data-slot="stats-value"
      className={cn(
        'font-serif text-4xl leading-none font-medium tracking-tight text-foreground sm:text-5xl',
        className,
      )}
      {...props}
    >
      <AnimatedNumber
        key={countUp ? 'count-up' : 'static'}
        value={value}
        startValue={countUp ? 0 : value}
        duration={duration}
        decimals={decimals}
        prefix={prefix}
        suffix={suffix}
        format={format}
      />
    </div>
  );
};

type StatsLabelProps = React.ComponentProps<'div'>;

const StatsLabel = ({ className, ...props }: StatsLabelProps) => {
  return <div data-slot="stats-label" className={cn('text-sm text-muted-foreground', className)} {...props} />;
};

interface StatsDeltaProps extends React.ComponentProps<'span'> {
  trend?: 'up' | 'down';
}

const StatsDelta = ({ trend = 'up', className, children, ...props }: StatsDeltaProps) => {
  const Icon = trend === 'up' ? ArrowUpRight : ArrowDownRight;

  return (
    <span
      data-slot="stats-delta"
      data-trend={trend}
      className={cn(
        'inline-flex items-center gap-1 text-xs whitespace-nowrap text-muted-foreground uppercase',
        className,
      )}
      {...props}
    >
      <Icon aria-hidden className="size-3 text-foreground rtl:-scale-x-100" />
      {children}
    </span>
  );
};

export {
  Stats,
  StatsHeader,
  StatsEyebrow,
  StatsTitle,
  StatsDescription,
  StatsGrid,
  StatsItem,
  StatsValue,
  StatsLabel,
  StatsDelta,
};

const ENTER =
  'animate-in fade-in slide-in-from-bottom-4 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';

const stagger = (index: number, step = 60, offset = 0): React.CSSProperties => ({
  animationDelay: `${offset + index * step}ms`,
});

const STATS = [
  {
    label: 'Deploys per week',
    value: 48000,
    suffix: '+',
    delta: '12% this quarter',
    trend: 'up',
  },
  {
    label: 'Median cold start',
    value: 38,
    suffix: 'ms',
    delta: '9ms faster',
    trend: 'down',
  },
  {
    label: 'Uptime, 90 days',
    value: 99.98,
    suffix: '%',
    decimals: 2,
  },
  {
    label: 'Teams on the platform',
    value: 2100,
    suffix: '+',
    delta: '310 new',
    trend: 'up',
  },
] as const;

const Stats01Block = () => {
  return (
    <Stats>
      <StatsHeader>
        <StatsEyebrow className={ENTER}>By the numbers</StatsEyebrow>
        <StatsTitle style={stagger(1)} className={ENTER}>
          Boring in production, on purpose.
        </StatsTitle>
        <StatsDescription style={stagger(2)} className={ENTER}>
          Every figure below comes from the public status page and is refreshed on the first of each month.
        </StatsDescription>
      </StatsHeader>

      <StatsGrid>
        {STATS.map((stat, index) => (
          <StatsItem key={stat.label} style={stagger(index, 60, 180)} className={ENTER}>
            <StatsValue value={stat.value} suffix={stat.suffix} decimals={'decimals' in stat ? stat.decimals : 0} />
            <StatsLabel>{stat.label}</StatsLabel>
            {'delta' in stat ? <StatsDelta trend={stat.trend}>{stat.delta}</StatsDelta> : null}
          </StatsItem>
        ))}
      </StatsGrid>
    </Stats>
  );
};

export default Stats01Block;
```
