WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/milestone-unlock-meter.json

Playground

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

62
1.2
<MilestoneUnlockMeter />

Installation

npx shadcn@latest add https://webberui.com/r/milestone-unlock-meter.json

Or, 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

PropTypeDefaultDescription
childrenReactNodePut Milestone children here
mode"data" | "scroll""data"Source that drives the level: data or scroll progress
valuenumber0Target fill percentage in data mode (0–100)
heightnumber440Height of the meter panel (px)
scrollLengthnumber2In 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
wavebooleantrueWhether to show the surface wave animation
durationnumber1.2Fill animation duration in data mode (seconds)
containerRefObject<HTMLElement | null>Ref of the nested scroll container for scroll mode
ariaLabelstring"Milestone unlock meter"Accessible label for the meter
classNamestringStyles for the outer container
panelClassNamestringStyles for the meter panel (border, radius, base color)
fillClassNamestringStyles for the liquid fill block

Milestone

PropTypeDefaultDescription
thresholdnumberUnlock threshold (0–100); it unlocks once the level rises above this value
titleReactNodeMilestone title
descriptionReactNodeMilestone description
iconReactNodeCheck markIcon shown once unlocked
classNamestringCard styles

How it works

  • Each Milestone is absolutely positioned at the percentage of the panel height matching its threshold, 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 threshold roughly between 10 and 95 is recommended, so cards display fully within the panel and are not clipped at the top or bottom edge.
  • data mode tweens to value with animate; scroll mode smooths the scroll progress with useSpring, 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 an aria-label; the milestones are expressed as an ordered list of <ol> / <li>, and each card's aria-label announces 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.

On this page