Stats & values
Stats Chart
A short introduction centered above a chart of percentages whose bars grow into view.
Layout and motion from the Nitip template — see it in contextimport { SpringButton } from "@/components/ui/spring-button";
import { StatBars } from "@/components/ui/stat-bars";
import { cn } from "@/lib/utils";
/*
* Stats Chart — a short introduction (label, heading, paragraphs, action) centered above a chart of percentages whose
* bars grow when it scrolls into view.
* Source: marcframe/nitip → components/sections/AboutSection.tsx ("Who are we?") + design-system Chart.
*
* Layout kept from Framer: the text is centered in a 48rem column, the paragraphs and the button in a 400px column
* under the heading; the chart runs the full container, 350px tall from 640px, stacked horizontal bars below.
* Motion kept (stat-bars): bars grow once half the chart is in view, starting 50ms apart, their height and shade
* following their value; stat-bars handles reduced motion. Dropped: Nitip's dotted section label (plain muted text
* here) and the ring cursor over the bars.
*/
export type StatsChartProps = {
/** Small line above the heading. */
label?: string;
title?: string;
paragraphs?: string[];
action?: { label: string; href: string };
/** Percentages (0–100) with their labels. */
stats?: { value: number; label: string }[];
className?: string;
};
const defaults = {
label: "Who we are",
title: "We combine creativity, data and honest storytelling to build brands people remember.",
paragraphs: [
"Northstar is a digital marketing studio of strategists, writers, designers and analysts. Since 2019 we have helped more than a hundred brands find their audience and keep it.",
"We work as one team with our clients: clear goals, weekly numbers and no work that doesn't earn its place.",
],
action: { label: "More about us", href: "/about" },
stats: [
{ value: 95, label: "Marketing strategy" },
{ value: 88, label: "Social media management" },
{ value: 100, label: "Search engine optimization" },
{ value: 76, label: "Paid advertising" },
{ value: 85, label: "Content & copywriting" },
],
};
export function StatsChart({
label = defaults.label,
title = defaults.title,
paragraphs = defaults.paragraphs,
action = defaults.action,
stats = defaults.stats,
className,
}: StatsChartProps) {
return (
<section className={cn("py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col items-center gap-16 px-(--gutter) md:gap-20 lg:gap-24">
<div className="flex w-full max-w-3xl flex-col gap-12">
<div className="flex flex-col gap-5">
{label && <p className="text-sm font-medium text-muted-foreground">{label}</p>}
<h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2>
</div>
<div className="flex max-w-100 flex-col items-start gap-12">
<div className="flex flex-col gap-5 text-muted-foreground">
{paragraphs.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
{action && (
<SpringButton href={action.href} arrow={false}>
{action.label}
</SpringButton>
)}
</div>
</div>
<StatBars stats={stats} height={350} />
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/stats-chart.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/spring-button.json https://framebrick.co/r/stat-bars.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/stats-chart.tsx.import { SpringButton } from "@/components/ui/spring-button"; import { StatBars } from "@/components/ui/stat-bars"; import { cn } from "@/lib/utils"; /* * Stats Chart — a short introduction (label, heading, paragraphs, action) centered above a chart of percentages whose * bars grow when it scrolls into view. * Source: marcframe/nitip → components/sections/AboutSection.tsx ("Who are we?") + design-system Chart. * * Layout kept from Framer: the text is centered in a 48rem column, the paragraphs and the button in a 400px column * under the heading; the chart runs the full container, 350px tall from 640px, stacked horizontal bars below. * Motion kept (stat-bars): bars grow once half the chart is in view, starting 50ms apart, their height and shade * following their value; stat-bars handles reduced motion. Dropped: Nitip's dotted section label (plain muted text * here) and the ring cursor over the bars. */ export type StatsChartProps = { /** Small line above the heading. */ label?: string; title?: string; paragraphs?: string[]; action?: { label: string; href: string }; /** Percentages (0–100) with their labels. */ stats?: { value: number; label: string }[]; className?: string; }; const defaults = { label: "Who we are", title: "We combine creativity, data and honest storytelling to build brands people remember.", paragraphs: [ "Northstar is a digital marketing studio of strategists, writers, designers and analysts. Since 2019 we have helped more than a hundred brands find their audience and keep it.", "We work as one team with our clients: clear goals, weekly numbers and no work that doesn't earn its place.", ], action: { label: "More about us", href: "/about" }, stats: [ { value: 95, label: "Marketing strategy" }, { value: 88, label: "Social media management" }, { value: 100, label: "Search engine optimization" }, { value: 76, label: "Paid advertising" }, { value: 85, label: "Content & copywriting" }, ], }; export function StatsChart({ label = defaults.label, title = defaults.title, paragraphs = defaults.paragraphs, action = defaults.action, stats = defaults.stats, className, }: StatsChartProps) { return ( <section className={cn("py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col items-center gap-16 px-(--gutter) md:gap-20 lg:gap-24"> <div className="flex w-full max-w-3xl flex-col gap-12"> <div className="flex flex-col gap-5"> {label && <p className="text-sm font-medium text-muted-foreground">{label}</p>} <h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2> </div> <div className="flex max-w-100 flex-col items-start gap-12"> <div className="flex flex-col gap-5 text-muted-foreground"> {paragraphs.map((paragraph) => ( <p key={paragraph}>{paragraph}</p> ))} </div> {action && ( <SpringButton href={action.href} arrow={false}> {action.label} </SpringButton> )} </div> </div> <StatBars stats={stats} height={350} /> </div> </section> ); }
Usage
import { StatsChart } from "@/components/stats-chart";
<StatsChart
label="Who we are"
title="We combine creativity, data and honest storytelling."
stats={[
{ value: 95, label: "Marketing strategy" },
{ value: 88, label: "Social media" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Who we are | Small muted line above the heading. |
| title | string | — | Section heading (h2). |
| paragraphs | string[] | — | Text in a 400px column under the heading. |
| action | { label, href } | More about us | Button under the paragraphs. |
| stats | { value: number, label }[] | 5 stats | Percentages (0–100); one bar each. |
| className | string | — | Classes for the <section>. |
- The text is centered in a 48rem column; the chart spans the container, 350px tall from 640px and stacked below.
- Bars grow once half the chart is in view, 50ms apart, their height and shade following their value (stat-bars, which handles reduced motion).
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS + Motion block called StatsChart (Next.js). - <section className="py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter), column centered, gap-16 / md:gap-20 / lg:gap-24. - Text (max-w-3xl, gap-12): a small muted label over an h2 (font-heading text-h2 semibold tracking-tight); then a 400px column of muted paragraphs and a shadcn-style button. - Chart: one bar per stat in a row 350px tall from 640px (items-end, gap-1.5), stacked horizontal bars below. Bars are rounded-md bg-primary text-primary-foreground with the value (text-xl semibold) and label; height = value% of the row and opacity following the value. When half the chart is in view the bars grow from 30% to their value, 50ms apart. Honour reduced motion yourself (the height comes from a CSS variable, which MotionConfig does not skip). - Only shadcn variables and block tokens.











