Page Sections

Count-Up Stats Hero

A dark hero React component: a left-aligned headline over 2–4 key figures. On entering the viewport the dividers draw in, each figure counts up from 0 in sequence, and an accent underline draws on completion.

Built for the first screen of a year-in-review, product home page or fundraising page: a left-aligned editorial headline and subtitle over a row of 2–4 key figures. When the row enters the viewport, the hairlines between cells draw in first, then each figure counts up from 0 with an easeOut curve (150ms apart), and a short accent underline draws under each number once it lands. A soft, low-opacity accent radial glow sits top right, all drawn in CSS with no images. It is always dark by default, or set it to auto to follow your site's theme.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add count-up-stats-hero.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

1600
600
Active edited usageDefault props
<CountUpStatsHero/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/count-up-stats-hero.en.json

Or, once registries are configured in components.json, install it as @webberui/count-up-stats-hero.

Usage

import { CountUpStatsHero } from "@/components/ui/count-up-stats-hero";

<CountUpStatsHero
  eyebrow="2026 年度回顧"
  title="三百間門市,用同一份班表開店。"
  subtitle="排班、調班與工時試算放在同一個畫面。"
  primaryAction={{ label: "預約導入說明", href: "/contact" }}
  secondaryAction={{ label: "看客戶案例", href: "/cases" }}
  stats={[
    { value: 320, suffix: "+", label: "合作門市", note: "分布在 14 個縣市" },
    { value: 12.6, decimals: 1, suffix: "萬", label: "每月排出的班次" },
    { value: 38, suffix: "%", label: "排班工時下降", note: "導入三個月中位數" },
  ]}
/>

Props

PropTypeDefaultDescription
titleReactNode—Main title (required)
statsCountUpStat[]—Key figures, 2–4 recommended; only the first 4 are shown
eyebrowReactNode—Small eyebrow above the title
subtitleReactNode—Subtitle, max width max-w-xl
as"h1" | "h2""h1"Heading level; use h2 when the page already has an h1
primaryActionCountUpStatsHeroAction—Primary CTA, filled with the accent color
secondaryActionCountUpStatsHeroAction—Secondary CTA, outlined
surface"dark" | "auto""dark"dark is always dark; auto follows the site's light/dark theme
minHeightnumber560Minimum section height (px)
durationnumber—Header entrance duration (ms), written to --wb-count-up-stats-hero-duration; 600ms when omitted
countDurationnumber—Count-up duration (ms), written to --wb-count-up-stats-hero-count; 1600ms when omitted
localestring"zh-TW" (English build: "en-US")Locale for number formatting (Intl.NumberFormat)
onCountComplete() => void—Called once every figure has finished counting
classNamestring—Merged into the root section

CountUpStat

FieldTypeDefaultDescription
valuenumber—Target value; the count starts from 0
labelstring—Small label under the number
prefixstring—Number prefix (e.g. +, NT$)
suffixstring—Number suffix (e.g. +, %, 萬 (ten thousand))
decimalsnumber0Decimal places, kept both while counting and at the final value
notestring—One-line note under the label

CountUpStatsHeroAction

FieldTypeDefaultDescription
labelstring—Button text
hrefstring—Renders a link when set, otherwise type="button"
onClick() => void—Click callback

How it works

  • Timing: the dividers draw in over 400ms first (horizontal scaleX on narrow screens, vertical scaleY from md up), then the figures count; each cell is offset by 150ms. The count runs on requestAnimationFrame with an easeOut curve 1-(1-t)^3, and Intl.NumberFormat locks the decimal places so no stray decimals flicker mid-count.
  • The count plays once, the first time the row enters the viewport. The header (eyebrow, title, subtitle, CTAs) rises and fades in on entering the viewport, 80ms apart.
  • CSS variables: --wb-count-up-stats-hero-duration (header entrance, 600ms), --wb-count-up-stats-hero-count (count, 1600ms), --wb-count-up-stats-hero-divider (dividers and underline, 400ms), --wb-count-up-stats-hero-ease (entrance curve, cubic-bezier(0.22,1,0.36,1)). The count reads the computed CSS variables when it starts, so global overrides also drive the JS count.
  • Override the accent with --wb-count-up-stats-hero-accent; when unset it is #2dd4bf on the dark surface and #0d9488 for auto in light mode. The glow, primary button fill, eyebrow dot and completion underline all follow it.

Accessibility

  • The root is a section labelled by the title via aria-labelledby; switch the heading to h2 with as to avoid several h1s on one page
  • Each number container's aria-label is the final value (with prefix and suffix) and the counting text is aria-hidden, so screen readers never announce mid-count numbers
  • Dividers, glow and underline are decorative and aria-hidden
  • CTAs are native links when href is set, otherwise type="button"; both show a focus-visible ring
  • When the system "reduce motion" setting is on, nothing counts: numbers and dividers render at their final state and the header uses a 150ms fade