Milestone Unlock Meter
A full-bleed progress meter — the liquid level rises to fill the whole section from data or scroll, and every time it crosses a threshold the matching milestone card pops open, unlocked and lit.
npx shadcn@latest add https://webberui.com/r/milestone-unlock-meter.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<MilestoneUnlockMeter />
Installation
npx shadcn@latest add https://webberui.com/r/milestone-unlock-meter.jsonOr, once registries are configured in components.json, install it as @webberui/milestone-unlock-meter.
Usage
Declare each threshold with a Milestone child; MilestoneUnlockMeter decides where the liquid level comes from based on mode.
Data mode
The level height is driven by value (0–100), tweening smoothly as the number changes, and cards whose threshold is crossed unlock automatically.
import {
Milestone,
MilestoneUnlockMeter,
} from "@/components/ui/milestone-unlock-meter";
import { Rocket, Star, Trophy, Zap } from "lucide-react";
<MilestoneUnlockMeter mode="data" value={64} accent="emerald" height={440}>
<Milestone threshold={12} title="Launch" description="Create your first project" icon={<Zap className="size-4" />} />
<Milestone threshold={40} title="Accelerate" description="Reach 100 users" icon={<Rocket className="size-4" />} />
<Milestone threshold={68} title="Standout growth" description="Hit 1,000 interactions" icon={<Star className="size-4" />} />
<Milestone threshold={92} title="Milestone reached" description="Unlock every achievement" icon={<Trophy className="size-4" />} />
</MilestoneUnlockMeter>Scroll mode
With mode="scroll", the level height is bound to the meter section's progress through the window (or the scroll container): scroll down and the level rises, unlocking each card as it passes its threshold. If the meter sits inside a nested overflow-y-auto container, pass that container's ref to container.
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
<MilestoneUnlockMeter mode="scroll" container={scrollerRef} height={300} scrollLength={2.4}>
{/* Milestone... */}
</MilestoneUnlockMeter>
</div>;Props
MilestoneUnlockMeter
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Put Milestone children here |
mode | "data" | "scroll" | "data" | Source that drives the level: data or scroll progress |
value | number | 0 | Target fill percentage in data mode (0–100) |
height | number | 440 | Height of the meter panel (px) |
scrollLength | number | 2 | In scroll mode, the scroll track length as a multiple of the panel height — larger means more scrolling |
accent | "emerald" | "blue" | "violet" | "amber" | "neutral" | "emerald" | Theme color |
wave | boolean | true | Whether to show the surface wave animation |
duration | number | 1.2 | Fill animation duration in data mode (seconds) |
container | RefObject<HTMLElement | null> | — | Ref of the nested scroll container for scroll mode |
ariaLabel | string | "Milestone unlock meter" | Accessible label for the meter |
className | string | — | Styles for the outer container |
panelClassName | string | — | Styles for the meter panel (border, radius, base color) |
fillClassName | string | — | Styles for the liquid fill block |
Milestone
| Prop | Type | Default | Description |
|---|---|---|---|
threshold | number | — | Unlock threshold (0–100); it unlocks once the level rises above this value |
title | ReactNode | — | Milestone title |
description | ReactNode | — | Milestone description |
icon | ReactNode | Check mark | Icon shown once unlocked |
className | string | — | Card styles |
How it works
- Each
Milestoneis absolutely positioned at the percentage of the panel height matching itsthreshold, and aligns its own center with the level, so the threshold tick, the ruler, and the card all line up precisely. - The level height, the ruler fill, and the progress readout share a single
MotionValue, updating frame by frame without triggering a React re-render; only when a threshold is crossed (the unlock state flips) is the matching card repainted. - Keeping
thresholdroughly between 10 and 95 is recommended, so cards display fully within the panel and are not clipped at the top or bottom edge. datamode tweens tovaluewithanimate;scrollmode smooths the scroll progress withuseSpring, making the level rise more fluidly.
Accessibility
- When the user has "reduce motion" enabled at the system level, the wave and pop animations are disabled and state changes complete instantly; the level still tracks the data or scroll, with only the surplus motion removed.
- The meter container carries
role="group"and anaria-label; the milestones are expressed as an ordered list of<ol>/<li>, and each card'saria-labelannounces the title, unlock state, and threshold percentage. - Purely decorative elements such as the liquid, wave crests, ruler, and progress readout are all marked
aria-hidden, so they do not interfere with assistive technology.
Spec Label Section
A product spec section styled like a nutrition label — rows stamp themselves in one by one as it scrolls into the window, and any row can expand to show notes and supporting links.
Balance Scale Compare
A balance-scale comparison layout — check feature chips onto either side and the two columns physically sink and rise with their accumulated weight, while the beam on its fulcrum tilts to an angle that reflects the real gap.