webdevpuneet.com

Scroll Milestone Confetti — Free One-Time Progress Burst Effect

Scroll Milestone Confetti · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="mc-bar" id="mcBar">
  <div class="mc-bar-fill" id="mcBarFill"></div>
  <div class="mc-bar-dot" data-pct="25" style="left:25%"></div>
  <div class="mc-bar-dot" data-pct="50" style="left:50%"></div>
  <div class="mc-bar-dot" data-pct="75" style="left:75%"></div>
  <div class="mc-bar-dot" data-pct="100" style="left:100%"></div>
</div>
<section class="mc-intro"><h1>Scroll ↓</h1><p>Four milestones ahead — each fires confetti the first time you pass it.</p></section>
<section class="mc-block" id="mcM25"><div class="mc-marker">25%</div><h2>Getting started</h2><p>You're a quarter of the way down the page.</p></section>
<section class="mc-block" id="mcM50"><div class="mc-marker">50%</div><h2>Halfway there</h2><p>Keep going — the next milestone is closer than the last.</p></section>
<section class="mc-block" id="mcM75"><div class="mc-marker">75%</div><h2>Almost done</h2><p>One more stretch to the finish.</p></section>
<section class="mc-block mc-final" id="mcM100"><div class="mc-marker">100%</div><h2>You made it!</h2><p>All four milestones unlocked.</p></section>
<canvas id="mcCanvas"></canvas>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

One-time milestone bursts
A fired flag prevents repeat confetti on re-scroll.
Canvas particle system
~90-particle bursts with gravity, drag, and rotation.
IntersectionObserver triggers
Cheap, threshold-based milestone detection.
Continuous progress fill
A top bar tracks exact scroll percentage.
Discrete unlock dots
Four dots light up as milestones are passed.
No confetti library
The whole particle system is plain canvas code.
Idle-cheap loop
An empty particle array costs almost nothing per frame.
Configurable milestones
Percentages and count are a simple array to edit.

About this UI Snippet

Scroll Milestone Confetti — Celebrating Scroll Progress at Real Milestones

Screenshot of the Scroll Milestone Confetti snippet rendered live

Rather than one big celebration at the end, this snippet marks four points down a long page — 25%, 50%, 75%, and 100% — and fires a small canvas confetti burst the first time the reader scrolls past each one, turning a long scroll into a small game with checkpoints. A fixed progress bar with four dots shows which milestones are already unlocked. Built with vanilla JavaScript, IntersectionObserver, and a lightweight canvas particle system — no confetti library required.

IntersectionObserver, fired once per milestone

Each milestone section is observed with a single IntersectionObserver at threshold: 0.4. When a milestone's isIntersecting flips true, the code checks a fired flag on that milestone's record before doing anything — so scrolling back up and passing the same milestone again does not double the confetti burst. This is the key difference from a plain scroll-position percentage check: each milestone independently remembers whether it has already celebrated.

A small, self-contained particle system

The confetti itself is a simple canvas-based particle simulation: each burst spawns ~90 rectangles with random angle, speed, color, and rotation, then a single requestAnimationFrame loop applies gravity, drag, and rotation to all live particles every frame, fading and removing each one once its lifespan elapses or it falls off-screen. No external confetti library is loaded — the whole effect is under 60 lines of canvas code.

Two independent progress signals

The top bar's fill width is driven by a continuous scroll-position percentage (scrollY / scrollable-height), updated on every scroll event, while the four dots along that same bar light up discretely based on the IntersectionObserver's one-time milestone unlocks. Reading both together — a smooth fill plus stepped unlocks — gives the reader both "how far am I" and "what have I actually earned" at a glance.

Cheap enough to run continuously

The particle loop runs every frame regardless of whether any particles are alive, but with an empty array the loop body is essentially a no-op clearRect, so there's no meaningful cost when no confetti is currently animating — no need to start and stop the rAF loop around each burst.

Customizing it

Change the milestone percentages or add more of them, swap the rectangle particles for circles or your brand's shapes, or trigger a bigger final burst at 100% than the earlier ones. Pair it with canvas confetti burst or a confetti celebration card for other confetti moments, or a scroll progress bar for the continuous-only version.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn intro, four milestone sections, a canvas, and a top bar render.
  2. 2
    Scroll past 25%A confetti burst fires and the first bar dot lights up.
  3. 3
    Keep scrolling50%, 75%, and 100% each fire their own one-time burst.
  4. 4
    Scroll back up and down againAlready-unlocked milestones do not re-fire confetti.
  5. 5
    Watch the top barThe fill tracks continuous scroll %, dots track unlocks.
  6. 6
    Add more milestonesAdd a section, a bar dot, and an entry in the milestones array.

Real-world uses

Common Use Cases

Long-form article rewards
Reward readers for reaching 25%, 50%, 75% and 100% of an article with a small canvas burst at each point.
Onboarding step celebrations
Celebrate completing each section of a guided tour, with a fired flag so confetti never repeats when scrolling back.
Fundraising progress pages
Mark funding milestones as visitors scroll, with a top progress bar tracking the exact percentage reached.
Course syllabus checkpoints
Signal lesson checkpoints down a long course syllabus page, using IntersectionObserver for cheap threshold detection instead of scroll handlers.
Playful landing pages
Pair with a canvas confetti burst for a page that celebrates progress, with about 90 particles per burst under gravity and drag.
Related: Scroll Progress Journey Trail
See the Scroll Progress Journey Trail for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

No. Each milestone record carries a fired boolean that gets set to true the first time its IntersectionObserver entry becomes intersecting, and the burst function is only called if that flag is still false. Scrolling back up and passing the same milestone again does nothing visually except the dot staying lit — the confetti is genuinely one-time per milestone per page load.

No — the burst is a self-contained canvas particle system: each burst pushes around 90 particle objects into an array with random velocity, color, and rotation, and a single requestAnimationFrame loop applies simple gravity and drag physics to whatever particles are currently alive, drawing them as small rotated rectangles. No external dependency is loaded.

The bar's fill width is a continuous value recalculated on every scroll event from scrollY divided by total scrollable height, so it moves smoothly with any scroll amount. The four dots on top of that bar are discrete — each one only changes state (from locked to unlocked) when its corresponding milestone's IntersectionObserver fires, so they update in steps rather than continuously.

Minimally — the requestAnimationFrame loop runs every frame regardless, but when the particles array is empty the loop body is just a canvas clearRect call with no iteration work, which is cheap enough that it does not need to be started and stopped around each burst. This keeps the code simpler than managing loop lifecycle per burst.

Render the milestone sections and canvas with refs, then in a mount effect create the IntersectionObserver scoped to those refs and start the requestAnimationFrame particle loop, storing the particles array and fired flags in refs so they persist without triggering re-renders. Return a cleanup that disconnects the observer, removes the scroll listener, and cancels the animation frame.