Stat Bars
Percentage bars that grow, one after another, when the chart scrolls into view.
From the Nitip template — see it in context94%
Projects delivered on time
78%
Clients who come back
62%
Traffic from organic search
import { StatBars } from "@/components/ui/stat-bars";
const stats = [
{ value: 94, label: "Projects delivered on time" },
{ value: 78, label: "Clients who come back" },
{ value: 62, label: "Traffic from organic search" },
];
export default function StatBarsDemo() {
return <StatBars stats={stats} height={220} className="w-full max-w-lg" />;
}Installation
$ npx shadcn@latest add https://framebrick.co/r/stat-bars.json1. Install the dependencies.
npm install motion clsx tailwind-merge2. Add
cn()tolib/utils.ts(skip if shadcn already created it).import { clsx, type ClassValue } from "clsx"; import { extendTailwindMerge } from "tailwind-merge"; /* tailwind-merge must know the site's type scale (globals.css) is a font size, not a text color — otherwise cn("text-h6 text-muted-foreground") drops text-h6. */ const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["display", "h1", "h2", "h3", "h4", "h5", "h6"] }] } }, }); export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into
components/ui/stat-bars.tsx."use client"; import * as React from "react"; import { motion, useReducedMotion, type Transition } from "motion/react"; import { cn } from "@/lib/utils"; /* * Stat Bars — percentage bars that grow when the chart scrolls into view (from the Nitip "Chart"). * Each bar's height and opacity follow its value; bars start at 30% and grow 50 ms apart (Nitip's refinement over * Framer, kept here). Below 640px the bars lie flat and their width follows the value instead. * They grow once, the first time they are seen. With reduced motion they simply appear at full size. */ const spring: Transition = { type: "spring", bounce: 0.2, duration: 0.4 }; export type Stat = { value: number; label: string }; export type StatBarsProps = { stats: Stat[]; /** Height of the chart from 640px up. */ height?: number; className?: string; }; export function StatBars({ stats, height = 320, className }: StatBarsProps) { // The bar's height comes from a CSS variable, and MotionConfig only skips transform and size animations, so // reduced motion is handled here: the bars start at their value instead of growing into it. const reduce = useReducedMotion(); return ( <div className={cn("flex w-full flex-col gap-1 sm:h-(--chart-height) sm:flex-row sm:items-end sm:gap-1.5", className)} style={{ ["--chart-height" as string]: `${height}px` }} > {stats.map((stat, index) => ( <motion.div key={stat.label} initial={{ ["--fill" as string]: reduce ? `${stat.value}%` : "30%" }} whileInView={{ ["--fill" as string]: `${stat.value}%` }} viewport={{ once: true, amount: 0.5 }} transition={reduce ? { duration: 0 } : { ...spring, delay: index * 0.05 }} style={{ opacity: Math.max(0.35, stat.value / 100), ["--value" as string]: `${stat.value}%` }} className="flex h-24 w-(--value) flex-col justify-between gap-6 overflow-hidden rounded-md bg-primary p-4 text-primary-foreground sm:h-(--fill) sm:w-auto sm:flex-1" > <div className="flex flex-col gap-3"> <p className="text-xl font-semibold tabular-nums">{stat.value}%</p> <div className="h-px bg-primary-foreground/25" /> </div> <p className="text-sm text-balance opacity-80">{stat.label}</p> </motion.div> ))} </div> ); }
Usage
import { StatBars } from "@/components/ui/stat-bars";
<StatBars stats={[{ value: 94, label: "Projects delivered on time" }, { value: 78, label: "Clients who come back" }]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| stats | { value: number; label: string }[] | — | Values are percentages (0–100). |
| height | number | 320 | Height of the chart from 640 px up, in px. |
| className | string | — | Classes for the row. |
- Bars start at 30% and grow 50 ms apart — Nitip's refinement over Framer, kept here.
- Below 640 px the bars lie flat and their width follows the value.
- They grow once, the first time they are seen.
- With reduced motion the bars appear at their value instead of growing.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.
Create a React + Tailwind CSS + Motion (motion/react) component called StatBars. - A row of bars, one per stat: bg-primary, text-primary-foreground, rounded-md, p-4, the value as a large tabular-nums percentage at the top, a hairline under it, and the label at the bottom. - Each bar's height is its value in percent of the chart height, and its opacity follows the value (minimum 0.35 so small bars stay readable). - Animate a CSS variable --fill from 30% to the value with whileInView (viewport once, amount 0.5) and a spring (bounce 0.2, duration 0.4s), delayed by 50ms per bar, with the bar's height reading height: var(--fill). - Under 640px the bars stack as horizontal rows whose width is the value instead. - With prefers-reduced-motion (useReducedMotion), start the bars at their value and use no transition — a CSS variable is not a transform, so MotionConfig would not skip it.











