You Might Also Like
Scroll Map Journey Story — Free GSAP ScrollTrigger SVG Route Animation
Scroll Map Journey Story · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Scroll Map Journey Story — A Route That Draws Itself as You Scroll

Travel features and expedition recaps often want more than a static map image — they want the route itself to feel traveled. This snippet draws an SVG trail progressively as the visitor scrolls, moves a marker along the path's real geometry (not an approximation), and swaps a waypoint story panel as the marker passes each stop.
`stroke-dashoffset` plus `pathLength` draws the line honestly
The route's real length is measured once with route.getTotalLength(), then set as both stroke-dasharray and the starting stroke-dashoffset — the classic SVG "line draw" technique. Every scroll update sets strokeDashoffset to routeLength * (1 - progress), so the visible drawn portion of the trail is always an exact, geometrically accurate fraction of the real path — not a rough approximation based on bounding-box width, which would draw unevenly across a winding route with tight curves.
The marker rides the path's actual geometry, not a straight-line lerp
Rather than interpolating the marker's position linearly between two fixed points (which would cut corners on every curve), route.getPointAtLength(routeLength * progress) asks the SVG path itself for the exact x/y coordinate at that fraction of its real length. The yellow marker therefore follows every bend of the trail precisely, speeding through straight stretches and slowing through tight curves exactly as the path's own geometry dictates.
Waypoints activate by progress threshold, matched to real stop positions
Each waypoint stop <g> carries a data-progress value matching where that stop actually sits along the path's length (not an arbitrary evenly-spaced guess), so a stop's dot lighting up green and the story panel's text changing both happen at the moment the marker visually reaches that exact point on the map — text and graphic stay physically synchronized.
One shared `progress` value, four synchronized outputs
A single self.progress inside onUpdate drives the dash-offset draw, the marker's coordinates, every stop's reached state, and the waypoint panel's text — the same "one number, several outputs" discipline used throughout this library's scroll-scrub snippets, which is what keeps every visual element honestly locked to the same scroll position instead of drifting apart under independent timers.
Customizing it
Redraw the d path with your own route geometry (a real city walk, a hiking trail, a delivery route) and update each stop's cx/cy to sit visually on the new path — then recompute each data-progress value by eyeballing roughly where along the new path's length each stop falls, or by sampling getPointAtLength for exact matches. Pair with a scroll company timeline for a date-based journey, or a scroll horizontal story track if the journey reads better left-to-right than as a pinned map.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how getTotalLength combined with stroke-dasharray and stroke-dashoffset produces an accurate progressive line-draw regardless of the path's curviness, and why getPointAtLength is the correct way to position a marker on a curved SVG path instead of interpolating between fixed coordinates. The same assistant can help extend the pattern — ask it to add a small distance-traveled counter that updates alongside the marker, animate the map's zoom level to focus on whichever segment is currently active, or replace the abstract SVG path with coordinates traced from a real geographic route. Treat the code as a working starting point for your own scroll-driven journey visualization.
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 map journey story" in plain HTML, CSS, and JavaScript using GSAP with its ScrollTrigger plugin, loaded from a CDN with no bundler.
Requirements:
- A pinned section containing an inline SVG with a faint background path representing a route, an identical foreground path that will be progressively drawn, several small circle-based waypoint markers positioned along the route at specific coordinates, and a single traveling marker circle — alongside a text panel showing an eyebrow label, a title, and a description for the current waypoint, preceded by an intro section and followed by an outro section.
- On page load, measure the foreground route path's real length using the SVG path element's getTotalLength method, and set both its stroke-dasharray and its initial stroke-dashoffset to that exact measured length, so the path's stroke starts completely hidden.
- Create a single ScrollTrigger on the pinned section with scrub enabled, and inside its onUpdate callback, set the route path's stroke-dashoffset to the measured length multiplied by one minus the current scroll progress, so the visible drawn portion of the path is always an accurate fraction of its real geometric length regardless of how curved the route is.
- In the same onUpdate callback, use the path element's getPointAtLength method (called with the measured length multiplied by the current progress) to compute the exact x/y coordinate the traveling marker should be positioned at, so the marker follows the path's true curved geometry rather than moving in a straight line between fixed points.
- Store an array of waypoint stops, each with a data attribute or JS value indicating roughly what fraction of the path's length it occupies, and toggle a "reached" visual state on each stop's marker once the current scroll progress passes that threshold — position values should roughly correspond to each stop's actual location along the drawn path.
- Also maintain an array of waypoint story entries, each with a progress threshold, an eyebrow label, a title, and descriptive text, and update the text panel to show whichever entry's threshold is the highest one the current scroll progress has already reached, so the story text changes at approximately the same moments the marker passes each visual waypoint.
- Ensure scrolling back up smoothly un-draws the path, retraces the marker backward along the exact same curved geometry, and reverts waypoint stops and story text, entirely through the scrub-driven ScrollTrigger with no separate reverse-direction logic.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
- 1Add GSAP and ScrollTriggerLoad both from the CDN and call gsap.registerPlugin(ScrollTrigger).
- 2Paste the HTML, CSS, and JSThe route path starts fully undrawn; only the faint background trail shows.
- 3Scroll into the pinned sectionThe trail draws itself while a marker travels along its exact geometry.
- 4Watch the waypoint stopsEach stop lights up and the story panel updates as the marker passes it.
- 5Scroll back upThe trail undraws and the marker retraces its path, since scrub is bidirectional.
- 6Swap in your own routeReplace the path's d attribute and recompute each stop's data-progress value.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The SVG path's real length is measured once with route.getTotalLength(), then used as both the stroke-dasharray and starting stroke-dashoffset — a technique that turns the path's stroke into one long dash exactly as long as the path itself, initially pushed fully out of view via the offset. Setting strokeDashoffset to routeLength times (1 minus progress) on every scroll update reveals the stroke proportionally to real path length, so the draw looks even and accurate across straight stretches and tight curves alike.
Interpolating linearly between two fixed x/y coordinates would cut straight across any curve between them, visibly leaving the drawn path. getPointAtLength(routeLength * progress) instead asks the SVG path element itself for the exact coordinate at that fraction of its real length, so the marker always sits precisely on the drawn line, following every bend correctly regardless of how curvy the route geometry is.
Each stop's data-progress value is set to roughly match where that stop's cx/cy coordinates actually fall along the path's total length, rather than being evenly spaced by index. Because both the stop-reveal check and the marker's position are compared against the same scroll progress value, a stop only lights up right around the moment the marker visually arrives at that point on the map.
The route length itself is remeasured automatically via getTotalLength() at page load, so the draw animation adapts to any new path shape with no manual length calculation. You do need to reposition each waypoint stop's cx/cy to sit on the new path visually, and re-estimate its data-progress value to roughly match where along the new path's length that point falls — either by eye or by sampling getPointAtLength at a few candidate values.
Create the ScrollTrigger inside a mount effect after the SVG has rendered and call route.getTotalLength() there too, since the path element must already exist in the DOM for length measurement to work. Update the marker's coordinates and stop states as direct DOM/attribute writes inside onUpdate rather than through component state, and call .kill() on the ScrollTrigger in the cleanup function.