Scroll Milestone Confetti — Free One-Time Progress Burst Effect

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's JS into an AI coding assistant like Claude and ask it to explain how the per-milestone fired flag combined with IntersectionObserver's isIntersecting check guarantees each confetti burst happens exactly once, even though the observer keeps firing every time a milestone's visibility changes in either direction. It's also useful for extending the particle system: ask it to make later milestones (say, 75% and 100%) spawn a bigger or differently-colored burst than earlier ones for an escalating sense of celebration, or to add a small popup toast next to each unlocked dot summarizing what was reached. It can also help you swap the rectangle particles for a shape that matches your brand, like small circles or a logo mark.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll milestone confetti" effect in plain HTML, CSS, and vanilla JavaScript (no libraries, no external confetti package).

Requirements:
- A long page (multiple full-height sections) with four clearly marked milestone sections positioned at roughly 25%, 50%, 75%, and 100% of the total scroll length.
- A fixed top progress bar showing continuous scroll progress (recalculated on scroll as scrollY divided by total scrollable height), with four small dot markers positioned along it at the 25/50/75/100% marks.
- Use a single IntersectionObserver (not four separate ones, and not a scroll-position percentage comparison) to detect when each milestone section becomes visible, with a reasonable threshold like 0.4.
- Track a "fired" boolean per milestone (e.g. in a small JS array of milestone records) and only trigger that milestone's confetti burst the first time its IntersectionObserver entry reports isIntersecting true and its fired flag is still false — confirm that scrolling back up and passing an already-fired milestone again does NOT trigger a second burst.
- When a milestone fires, mark its corresponding progress-bar dot as "unlocked" (a visible style change) and spawn a confetti burst: a self-written canvas particle system, not an external library. Each burst should create ~80-100 small rectangle or circle particles with randomized angle, speed, color, and rotation, and a single continuously-running requestAnimationFrame loop should apply gravity and air drag to all currently-alive particles, fading and removing each one once its lifespan expires or it falls off the bottom of the screen.
- Ensure the particle loop is cheap to run even when no particles are alive (an empty-array frame should do negligible work) so it doesn't need to be manually started and stopped around each burst.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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 articles
Reward readers for making progress through the piece.
Onboarding tours
Celebrate completing each step of a guided page.
Fundraising pages
Mark funding milestones as visitors scroll a campaign.
Course pages
Signal lesson checkpoints down a long syllabus page.
Product changelogs
Celebrate reaching major version sections.
Playful landing pages
Pair with canvas confetti burst for a final CTA.

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.