docs / patterns / stat-bento
StatBento
a strip of headline numbers, each a big value with its unit in an accent suffix and a short label beneath. a proof strip to sit under a hero or a case study.
intensity · structuresrsc · server
provenance
shaped from personal-websitelayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
a proof strip on a landing section, under a hero or a CaseStudyHerothree cells reads best. cells take a translucent ink fill, safe inside a Cardnot a dashboard. live metrics with trend and spark are StatCardnever mix cell types. a mixed grid is BentoGrida11y
plain text in a grid · icons are decorative andaria-hiddenjest-axedependencies
BentoMetric from the same packagelive demo · try it out
12M
180+
60%
customize
animatecount each value up from zero on mount
usage
import { StatBento } from "usva/patterns/stat-bento";
<StatBento animate
stats={[
{ value: "12", suffix: "M", label: "downloads" },
{ value: "180", suffix: "+", label: "contributors" },
{ value: "60", suffix: "%", label: "less code" },
]}
/>props
| prop | type | default | notes |
|---|---|---|---|
| stats | StatBentoItem[] | — | the cells: { value, label, suffix?, icon? }. the suffix carries the unit in accent-alt. |
| animate | boolean | — | counts each numeric value up from zero on mount. |
| as | React.ElementType | "div" | the element rendered as the grid. pass RevealGroup to stagger the cells: it animates its direct children, so it must be the grid, not wrap it. |
get it
npx shadcn add https://usva.build/r/stat-bento.jsonsource · components/ui/stat-bento.tsxexactly what this command copies
import type * as React from "react";
import { cn } from "@/lib/utils";
import { BentoMetric } from "./bento-grid";
export interface StatBentoItem {
value: React.ReactNode;
label: React.ReactNode;
/** Trailing unit on the value, keyed to the alternate accent. */
suffix?: React.ReactNode;
icon?: React.ReactNode;
}
interface StatBentoOwnProps<T extends React.ElementType> {
stats: StatBentoItem[];
/** Count each numeric value up from zero on mount. */
animate?: boolean;
/**
* Element rendered as the grid. Pass `RevealGroup` to stagger the cells: it
* animates its direct children, so it has to be the grid rather than wrap it.
*/
as?: T;
}
export type StatBentoProps<T extends React.ElementType = "div"> =
StatBentoOwnProps<T> &
Omit<React.ComponentPropsWithoutRef<T>, keyof StatBentoOwnProps<T>>;
/**
* A standalone strip of headline numbers, not a bento of mixed cells: every child is
* a display-weight metric. It stays motion-free so the copied registry source has no
* import to resolve. Polymorphic rather than a forwardRef so `as` can carry the props
* of whatever element renders the grid.
*
* Cells take a translucent ink fill rather than `bg-surface`, which would vanish the
* moment the strip sits inside a Card that already paints that surface.
*/
export function StatBento<T extends React.ElementType = "div">({
className,
stats,
animate,
as,
...props
}: StatBentoProps<T>) {
const Comp = (as ?? "div") as React.ElementType;
return (
<Comp
className={cn("grid grid-cols-1 gap-4 sm:grid-cols-3", className)}
{...props}
>
{stats.map((stat, index) => (
<BentoMetric
key={typeof stat.label === "string" ? stat.label : index}
size="lg"
animate={animate}
value={stat.value}
suffix={stat.suffix}
label={stat.label}
icon={stat.icon}
className="rounded-2xl bg-ink/[0.04] sm:p-8"
/>
))}
</Comp>
);
}
StatBento.displayName = "StatBento";