← StatsOpen in v0
Stats 02 — Narrative Stats
Split layout pairing an eyebrow, headline, description and CTA link with a 2×2 grid of large animated numbers, labels and supporting copy.
$ npx shadcn@latest add @ludusvibe/stats-02Code
Installed to components/sections/stats-02.tsx — it's yours to edit.
"use client"
import * as React from "react"
import { ArrowRight } from "lucide-react"
import { buttonVariants } from "@/components/ui/button"
import { AnimatedNumber } from "@/components/ludus/animated-number"
import { Reveal } from "@/components/ludus/reveal"
import { cn } from "@/lib/utils"
type Cta = { label: string; href: string }
type Stat = {
value: number
label: string
description: string
prefix?: string
suffix?: string
/** Intl.NumberFormat options, e.g. `{ notation: "compact" }` for 120M-style values. */
format?: Intl.NumberFormatOptions
}
export type Stats02Props = {
eyebrow?: string
title?: React.ReactNode
description?: string
cta?: Cta
/** Rendered as a 2×2 grid of animated numbers. */
stats?: Stat[]
/** Heading element — use "h1" when this is the page's main heading. */
as?: "h1" | "h2"
className?: string
}
const defaultStats: Stat[] = [
{
value: 8700,
suffix: "+",
label: "Customers worldwide",
description: "Teams of every size run on Lumen, across 60+ industries.",
},
{
value: 3.4,
suffix: "×",
format: { maximumFractionDigits: 1 },
label: "Faster time to insight",
description: "Median across teams migrating from legacy BI stacks.",
},
{
value: 120_000_000,
format: { notation: "compact" },
label: "Events every day",
description: "Ingested, enriched and ready to query in seconds.",
},
{
value: 99.98,
suffix: "%",
format: { maximumFractionDigits: 2 },
label: "Uptime, last 12 months",
description: "Backed by an enterprise SLA and 24/7 on-call support.",
},
]
export function Stats02({
eyebrow = "By the numbers",
title = "Growth that speaks for itself",
description = "Lumen powers product decisions at thousands of companies — from two-person startups to global enterprises. Here's what that looks like in practice.",
cta = { label: "Read customer stories", href: "#" },
stats = defaultStats,
as: Heading = "h2",
className,
}: Stats02Props) {
return (
<section className={cn("bg-background px-6 py-24", className)}>
<div className="mx-auto grid max-w-6xl items-center gap-14 lg:grid-cols-2 lg:gap-20">
<div>
<Reveal>
<p className="text-sm font-medium tracking-widest text-primary uppercase">
{eyebrow}
</p>
</Reveal>
<Reveal delay={0.1}>
<Heading className="mt-4 max-w-lg text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
{title}
</Heading>
</Reveal>
<Reveal delay={0.2}>
<p className="mt-5 max-w-lg text-pretty text-muted-foreground md:text-lg">
{description}
</p>
</Reveal>
<Reveal delay={0.3}>
<a
href={cta.href}
className={cn(
buttonVariants({ size: "lg", variant: "outline" }),
"group mt-8"
)}
>
{cta.label}
<ArrowRight className="transition-transform group-hover:translate-x-0.5" />
</a>
</Reveal>
</div>
<div className="grid gap-x-8 gap-y-10 sm:grid-cols-2">
{stats.map((stat, i) => (
<Reveal key={i} delay={0.1 + i * 0.1} className="border-t pt-6">
<div className="font-heading text-4xl font-semibold tracking-tighter md:text-5xl">
<AnimatedNumber
value={stat.value}
prefix={stat.prefix}
suffix={stat.suffix}
format={stat.format}
/>
</div>
<div className="mt-3 font-medium">{stat.label}</div>
<p className="mt-1 text-sm text-pretty text-muted-foreground">
{stat.description}
</p>
</Reveal>
))}
</div>
</div>
</section>
)
}