
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.
@hirael
registry/hirael/bases/radix/blocks/stats-01/stats-01.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;



























