More Scroll Snippets
Lottie Scroll Donut Chart — Segments Fill on Scroll
Lottie Scroll Donut Chart · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Filled Lottie Donut Chart With Sequential Trim-Path Segments

The Lottie Scroll Donut Chart snippet fills a multi-colour donut one segment at a time as the visitor scrolls — each coloured slice sweeping in from where the last one ended — and empties it again on the way up. Nothing plays on a timer; the scrollbar drives the fill frame by frame, and the whole chart is generated in JavaScript, so the way each segment draws is fully visible in the code.
Lottie is JSON, assembled in code
A Lottie animation is a JSON document of vector layers and keyframes. Instead of fetching one, this snippet builds the animationData object directly and passes it to lottie.loadAnimation, so there is nothing to host and nothing that can 404. A segment helper stamps out each slice as a layer, and a keyframe helper assembles the draw with the easing tangents lottie-web requires.
Every segment is a trimmed ring
The donut is not four separate arcs drawn at different angles — it is four copies of the same thick stroked ring, each trimmed to a different slice of the circumference. A Lottie trim path (shape type tm) renders only a portion of a stroked path, from a start percentage to an end percentage. For segment two, for example, the trim start is fixed at forty percent and the trim end animates from forty to sixty-eight percent, so it draws only its own slice, beginning exactly where segment one ended. Every layer carries a minus-ninety-degree rotation so that zero percent starts at the top of the circle, the way a donut chart is conventionally read.
Drawing the segments in sequence
Each segment fills during its own window of the timeline. The windows are sized in proportion to the segment values and laid end to end, so the first slice draws over the opening frames, the second picks up right after, and so on until the ring is complete. Before a segment's window its trim end sits at its start percentage, so the slice is invisible until its turn; after the window it holds at full, so it stays drawn. The result is a donut that fills slice by slice in order rather than all at once.
The one keyframe detail that matters
When you author a Lottie by hand, every animated keyframe must carry its i (in) and o (out) bezier tangents. Omit them and lottie-web treats the keyframe as a hold keyframe: it never interpolates, so a trim end keyframed to grow stays stuck at its start and the slice never draws. The keyframe helper here attaches linear tangents to every keyframe, which is what makes the segments fill.
Autoplay off, scroll on
The animation is loaded with autoplay: false so its internal clock never runs. A GSAP ScrollTrigger pins the stage and scrubs a single plain value from 0 to 1 over a range several viewport-heights tall, and a requestAnimationFrame loop reads that value every frame and calls anim.goToAndStop(p * (totalFrames - 1), true). The true tells lottie-web the value is a frame number, and goToAndStop renders a single still frame without starting playback, so the fill is welded to scroll position. Because lottie-web interpolates between keyframes, fractional frames render smoothly.
Reversible for free
Deriving the frame from one scrubbed value means scrolling up produces smaller values, so the segments un-draw in reverse order with no extra code. A smoothed scrub: 0.5 lets the fill glide toward the scroll position rather than snapping to every wheel tick, and the caption fades out the moment the first slice appears. Pair it with the Lottie scroll bar chart or the Lottie scroll scrub ring for a fuller data reveal.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not have to reverse-engineer how a JSON file becomes a scroll-filled donut. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to walk through how each segment is trimmed to its own slice, or why every keyframe needs i/o tangents. The same assistant can help you extend it — ask it to add a percentage label in the centre, a legend beside the donut, feed the VALUES from real data, or animate a slice popping out on the way in. Treat the code as a conversation starter, not a finished artifact.
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:
Build a "scroll-filled Lottie donut chart" in plain HTML, CSS, and JavaScript using lottie-web and GSAP's ScrollTrigger plugin, both from a CDN (no bundler).
Requirements:
- A pinned full-height section with a centered container div for the Lottie, an intro caption overlay, and a live "percent filled" read-out.
- Build the Lottie as an inline animationData JSON object (leave a comment showing how to swap it for path: 'your-animation.json'). Include a keyframe helper that ALWAYS attaches i/o easing tangents (linear) — without them lottie-web holds the value and nothing draws.
- The animation is a donut made of several coloured segments. Each segment is a copy of the same thick stroked ring, trimmed to its slice: the trim start is fixed at the segment's beginning percentage and the trim end animates from that start to its end. Rotate every segment layer by -90 degrees so 0% is at the top. Draw the segments in sequence, each over a timeline window sized in proportion to its value and laid end to end. Add a faint full-circle track behind them.
- Load with lottie.loadAnimation using renderer 'svg', loop false, autoplay FALSE.
- Register a GSAP tween on a ScrollTrigger targeting the pinned section, with pin true, start at top top, scrub around 0.5, and an end several hundred percent tall, animating one value p from 0 to 1.
- In a requestAnimationFrame loop, read p and call anim.goToAndStop(p * (anim.totalFrames - 1), true), update the read-out, and fade the caption out once p passes a small threshold.
- Confirm scrolling up un-draws the segments in reverse.Step by step
How to Use
- 1Load the three CDN scriptsAdd lottie.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
- 2Paste the HTML, CSS, and JSA faint track ring appears centred in a pinned stage with a live "% filled" read-out.
- 3Scroll downThe coloured segments draw in one after another around the donut, tied directly to scroll position.
- 4Scroll back upThe segments un-draw exactly, because goToAndStop seeks a single frame rather than playing on a timer.
- 5Set your own dataEdit the VALUES array (fractions that sum to 1) and the COLORS array to plot your own breakdown.
- 6Tune the fillChange the ScrollTrigger end value (+=400%) for a longer, slower fill or a shorter, quicker one.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Every segment is a copy of the same thick stroked ring, but its trim path start is fixed at the segment beginning and its trim end animates from that start to the segment end. So segment two, starting at forty percent, has its trim end grow from forty to sixty-eight percent — it renders only that slice, beginning exactly where segment one finished. A minus-ninety-degree rotation on each layer puts zero percent at the top.
Because lottie-web treats an animated keyframe with no i/o easing tangents as a hold keyframe — it never interpolates, so a trim end keyframed to grow stays stuck at its start and the slice never draws. The keyframe helper in this snippet attaches linear tangents to every keyframe. If you hand-edit a keyframe and a slice stops drawing, missing tangents are the first thing to check.
Edit the VALUES array so its entries are fractions that sum to 1, and the COLORS array to match. The segment start percentages, timeline windows, and draw order all derive from those values, so they adapt automatically — add or remove entries to change the number of slices. Keep the values summing to 1 so the ring closes cleanly at full scroll.
play runs the animation on its internal clock, which is exactly what you do not want when scroll should control it. goToAndStop(frame, true) renders one specific still frame and never starts the timer — the second argument tells lottie-web the value is a frame number, not milliseconds. Multiplying scroll progress by totalFrames gives the frame to show, and fractional frames interpolate smoothly so the fill is continuous.
Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind version. Call lottie.loadAnimation against a ref inside a mount effect, register the ScrollTrigger there, and on cleanup call anim.destroy() and kill the ScrollTrigger instance (or revert a gsap.context) so the SVG and scroll listener are released on unmount.