Source Code

<div class="demo">
  <div class="route-card">
    <svg class="route-svg" viewBox="0 0 300 120" preserveAspectRatio="none">
      <path id="route-path" class="route-track" d="M 20 90 C 80 90, 70 20, 130 30 S 220 90, 280 40" />
      <path id="route-fill" class="route-fill" d="M 20 90 C 80 90, 70 20, 130 30 S 220 90, 280 40" />
      <circle id="route-pin" class="route-pin" r="6" />
      <circle class="route-endpoint" cx="20" cy="90" r="4.5" />
      <circle class="route-endpoint" cx="280" cy="40" r="4.5" />
    </svg>
    <div class="route-info">
      <div class="route-labels">
        <span class="route-label start">Warehouse</span>
        <span class="route-label end">Your door</span>
      </div>
      <div class="route-meta">
        <span class="route-pct" id="route-pct">0%</span>
        <span class="route-eta" id="route-eta">Calculating ETA…</span>
      </div>
    </div>
  </div>
</div>

Route Tracking Progress Loader — Free HTML CSS JS Snippet

Route Tracking Progress Loader · Loaders · Plain HTML, CSS & JS · Live preview

What's included

Features

Pin position computed every frame with the native SVG getPointAtLength() API — exact, not approximated
Traveled-trail reveal uses the same stroke-dasharray/dashoffset technique as SVG progress rings, applied to an arbitrary curve
getTotalLength() measures the exact pixel length of a Bézier curve automatically, no manual math required
Pin position and dashoffset reveal are both driven by one shared progress fraction, so they never drift out of sync
Randomized per-frame increment gives a naturally uneven pace instead of a robotic linear count-up
Live ETA countdown derived from the same progress value against a configurable total-time budget
Fully reusable for any curved route shape — just edit the shared path d attribute
No external mapping library, image asset, or animation library required

About this UI Snippet

Route Tracking Progress Loader — A Pin Animated Along an SVG Path with getPointAtLength

Screenshot of the Route Tracking Progress Loader snippet rendered live

Delivery and ride-tracking apps share a familiar visual language: a pin sliding along a curved route between two points, with the traveled portion of the path filled in behind it. This loader recreates that exact effect for any process that maps naturally onto "progress from A to B" — a shipment update, a multi-step pipeline, an onboarding journey — using two small SVG APIs, getTotalLength() and getPointAtLength(), instead of a generic percentage bar.

Measuring and pre-filling the path

The route is a single curved SVG <path> (built with cubic Bézier C/S commands) rendered twice on top of each other: a light gray .route-track showing the full route at all times, and a colored .route-fill on top representing only the traveled portion. On load, pathTrack.getTotalLength() measures the exact pixel length of the curve — impossible to know from the path's d attribute alone since it's a curve, not a straight line — and that length is used to set .route-fill's stroke-dasharray to exactly one segment as long as the whole path, with stroke-dashoffset initially equal to that same length so nothing is visible yet. This is the same masking technique used by SVG progress rings, applied here to an arbitrary curved path instead of a circle.

Revealing the traveled portion

As progress (a 0-to-1 fraction) increases, setProgress() sets stroke-dashoffset to totalLen * (1 - t), progressively revealing more of the .route-fill stroke from its start point exactly as t grows — at t = 0 the full length is hidden, at t = 1 the offset is zero and the entire colored path is visible, tracing precisely along the curve rather than a straight line between the two endpoints.

Placing the pin exactly on the curve with getPointAtLength

The moving pin's position is the trickiest part to get right without a library, because a curved path's x/y coordinates at "40% of the way along" cannot be computed with simple linear interpolation between the endpoints — the curve bows away from the straight line between them. pathTrack.getPointAtLength(totalLen * t) solves this directly: given a distance along the path, the browser's own SVG geometry engine returns the exact {x, y} point at that distance, which is assigned straight to the pin <circle>'s cx/cy attributes every frame. Because both the fill's dash offset and the pin's position are driven by the same totalLen * t calculation, the pin always sits exactly at the leading edge of the filled trail, never lagging behind or overshooting it.

Progress simulation with a live ETA

A requestAnimationFrame loop increments progress by a small randomized amount each frame (0.0022 + Math.random() * 0.0016), giving a naturally uneven, non-robotic pace rather than a perfectly linear count-up — closer to how a real delivery's reported progress fluctuates. Alongside the percentage, etaEl derives a remaining-minutes estimate directly from the same t value against a fixed TOTAL_MINUTES budget, so the numeric ETA and the visual pin position always stay in sync, both driven by one single source-of-truth progress value, and flips to "Arrived" once t reaches 1.

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 why getPointAtLength() is necessary instead of simply interpolating between the route's start and end coordinates, and how that relates to the stroke-dasharray/dashoffset technique used for the trail fill sharing the same underlying path length. It's also a good candidate for extension — ask it to add a small vehicle or delivery-truck icon that rotates to face its direction of travel along the curve (using the path's tangent angle at each point), multiple simultaneous pins tracking several shipments on the same route, or a version that reads real progress from a WebSocket or polling API instead of simulated increments.

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 route-tracking progress loader in HTML, CSS and vanilla JavaScript using inline SVG — no external mapping library.

Requirements:
- Draw a single curved SVG path (using cubic Bézier commands, not a straight line) representing a route between two labeled endpoints, rendered twice: once as a static light-gray full-route track, and once as a colored "traveled" overlay using the exact same path shape.
- On load, measure the path's exact total length using the SVG getTotalLength() method, and use that measured length to set up a stroke-dasharray/stroke-dashoffset masking technique on the colored overlay path so it starts fully hidden.
- Animate a 0-to-1 progress value that reveals the colored overlay path proportionally (via stroke-dashoffset) as it increases, and independently position a circular "pin" element exactly on the curve at the corresponding distance using the SVG getPointAtLength() method — not linear interpolation between the endpoints — so the pin always sits precisely at the leading edge of the revealed trail.
- Advance the progress value using requestAnimationFrame with a small randomized per-frame increment so the pace feels natural rather than perfectly linear, and stop cleanly once progress reaches 100%.
- Show a live percentage readout and a live estimated-time-remaining countdown, both derived from the same underlying progress fraction so they never fall out of sync with each other or with the pin's visual position, and switch the ETA text to an "arrived" state once progress completes.

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
    Watch the pin travelThe pin animates along the exact curve of the SVG path using getPointAtLength(), with the trail behind it filling in via a stroke-dashoffset reveal.
  2. 2
    Read the live percentage and ETABoth are derived from the same 0-to-1 progress value each frame, so they always stay in sync with the pin's visual position.
  3. 3
    Redraw the routeEdit the d attribute on both #route-path and #route-fill (they must match exactly) to change the route's shape — getTotalLength() automatically re-measures whatever curve you draw.
  4. 4
    Change the total ETA budgetAdjust the TOTAL_MINUTES constant to change what "100%" represents in real time for the ETA calculation.
  5. 5
    Replace simulated progress with real progressInstead of incrementing progress by a random amount each frame, call setProgress(t) directly with a real fraction reported by your tracking API or backend.
  6. 6
    Relabel the endpointsUpdate the start and end text in .route-labels to match your use case — a delivery route, a pipeline stage name, or an onboarding step.

Real-world uses

Common Use Cases

Delivery and order tracking screens
Show a shipment moving from warehouse to doorstep with a live ETA, the standard visual pattern used by delivery and food-order tracking apps.
Multi-stage pipeline or job progress
Represent an abstract process (a build pipeline, a data import, a multi-stage approval) as a journey along a path rather than a flat bar, giving it more narrative weight.
ONBOARD
Onboarding or setup journey visualization
Pair with a multi-step onboarding flow to show overall journey completion in a more visually engaging way than a plain step progress bar.
Ride-sharing and logistics ETA widgets
Reuse the same path-following pin technique for a ride-tracking or logistics dashboard widget showing a vehicle en route to a destination.

Got questions?

Frequently Asked Questions

It uses the native SVG method pathTrack.getPointAtLength(distance), which the browser computes directly from the path's actual geometry. Multiplying the path's total length (from getTotalLength()) by the current 0-to-1 progress fraction gives the exact distance to query, so the returned x/y point always lies precisely on the curve.

The two paths share the exact same d attribute so they trace identical curves, but serve different visual roles: .route-track is a static light-gray path always fully visible to show the whole route, while .route-fill uses stroke-dasharray/dashoffset to reveal only the traveled portion on top of it, in the accent color.

Update the d attribute on both #route-path and #route-fill to the same new curve. Because getTotalLength() re-measures whatever path is present on load, the dash-based reveal and the pin's getPointAtLength() calls automatically adapt to the new curve's actual length and shape with no other changes needed.

etaEl computes Math.round(TOTAL_MINUTES * (1 - t)) using the same t (0-to-1 progress) value that drives the pin position and trail fill, so the displayed minutes-remaining count is always mathematically consistent with how far the pin has visually traveled.

Remove or stop the requestAnimationFrame tick() loop and instead call setProgress(t) directly whenever your backend reports a new completion fraction (for example, distance traveled divided by total route distance), passing a value between 0 and 1.

Yes — getPointAtLength() and getTotalLength() operate in the SVG's internal viewBox coordinate system, not screen pixels, so the pin stays correctly positioned on the curve regardless of how large or small the SVG is rendered on screen.