CSS Motion Path: Animate Elements Along a Custom SVG Path
Learn how to animate any HTML element along a custom SVG path using CSS Motion Path — offset-path, offset-distance, and offset-rotate explained with real examples.
What Is CSS Motion Path and Why Should You Care
CSS Motion Path — officially the CSS Motion Path Module Level 1 spec — lets you move any HTML element along an arbitrary path defined in SVG or with basic shape functions. No JavaScript. No canvas. No GSAP license required if you don't want one. You define a path, you animate offset-distance from 0% to 100%, and the browser does the rest.
Before this landed in Chromium 46 back in 2016, animating something along a curve meant either a canvas hack, a JavaScript animation loop that computed bezierPoint() on every frame, or the old SVG animateMotion element — which worked but was painful to compose with CSS transitions. Motion Path collapses all of that into three CSS properties. Honestly, it's one of those specs that makes you wonder why it took so long.
Worth noting: browser support is excellent as of 2026. Chrome, Edge, Opera, and Samsung Internet have had full support for years. Firefox shipped offset-path with SVG path values in Firefox 112 (2023). Safari joined the party in Safari 16 with offset-path: path(). You can ship this in production without a polyfill for the vast majority of users.
The practical applications are wider than you'd think. Think icon badges orbiting a profile photo, a loading indicator that traces a logo shape, a card that slides in along a curved arc instead of a boring straight line, or a particle system where each dot follows a lissajous figure. If you've been reaching for a full animation library just for curved movement, Motion Path probably does the job in 10 lines of CSS.
The Three Core Properties: offset-path, offset-distance, offset-rotate
offset-path is where you define the track. You can pass a raw SVG path string using path('M 0 0 C 100 0 100 100 200 100'), reference an SVG <path> element in the DOM with url(#myPath), or use CSS basic shapes like circle(), ellipse(), or polygon(). The path coordinates are relative to the element's containing block, not the SVG coordinate space — which trips people up the first time.
offset-distance is the scrubber. It's a percentage (or length) that says how far along the path the element sits. 0% puts it at the start, 100% puts it at the end. Animate this property and the element moves. That's genuinely the whole mechanism. Combine it with animation-timing-function: cubic-bezier() for easing, or linear() with explicit stops if you want spring-like behavior.
offset-rotate controls the element's heading as it travels. The default value is auto — the element rotates to face the direction of travel, which is what you want for arrows or car icons. Set it to auto 90deg if your artwork is pointing up instead of right. Set it to 0deg if you want the element to stay upright (useful for text labels or UI cards that should never tilt). In practice, auto is what you'll use 90% of the time.
There's also offset-anchor, which shifts which point of the element sits on the path. Default is auto, which maps to transform-origin. If you're animating a 48px icon and want its visual center — not its top-left corner — on the path, either set offset-anchor: 50% 50% or just make sure your transform-origin is already centered.
/* The full shorthand — not widely used but good to know */
.orbiting-dot {
offset: path('M 200 100 A 100 100 0 1 1 200 99.9') auto 0% auto;
}
/* Longhand is clearer for anything non-trivial */
.orbiting-dot {
offset-path: path('M 200 100 A 100 100 0 1 1 200 99.9');
offset-rotate: auto;
offset-distance: 0%;
offset-anchor: 50% 50%;
animation: orbit 3s linear infinite;
}
@keyframes orbit {
to { offset-distance: 100%; }
}Building a Real Example: Element Following an SVG Path
Let's build something concrete. Say you want a glowing dot to trace a wave shape across a hero section. You'd draw the SVG path in Figma (or by hand), export the d attribute, then paste it into your CSS. Here's the full pattern:
<!-- You don't need the SVG to be visible — just defined -->
<svg width="0" height="0" style="position:absolute">
<defs>
<path
id="wave-path"
d="M -60 200 C 80 50 160 350 320 200 S 560 50 640 200 S 880 350 960 200"
/>
</defs>
</svg>
<div class="scene">
<div class="dot"></div>
</div>.scene {
position: relative;
width: 960px;
height: 400px;
overflow: hidden;
}
.dot {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #a78bfa, #7c3aed);
box-shadow: 0 0 12px 4px rgba(124, 58, 237, 0.6);
/* Motion path setup */
offset-path: url(#wave-path);
offset-rotate: 0deg; /* keep dot upright, don't tilt it */
offset-anchor: 50% 50%;
animation: trace-wave 4s ease-in-out infinite alternate;
}
@keyframes trace-wave {
from { offset-distance: 0%; }
to { offset-distance: 100%; }
}Quick aside: the alternate fill mode means the dot reverses back along the same path on every other iteration, so you get a natural back-and-forth without a jarring jump cut. If you want it to loop seamlessly instead, close the path (Z at the end or match start and end coordinates) and use infinite without alternate.
One more thing — the hidden SVG trick works perfectly, but if you're in a React component with CSS-in-JS or Tailwind, the url(#wave-path) reference can break due to scoping. Either inline the path string directly with offset-path: path('M -60 200 C ...'), or make sure your SVG's <defs> are in the document root, not inside a shadow DOM.
Combining Motion Path With CSS Animations You Already Know
Motion Path doesn't operate in isolation — it composes cleanly with transform, opacity, filter, and every other animatable CSS property. This is where it gets genuinely fun. You can stack a scale transform on top of the motion path so your element grows as it reaches the midpoint, or fade it in at the start and out at the end with opacity keyframes, all while it's tracing your curve.
@keyframes trace-and-pulse {
0% { offset-distance: 0%; opacity: 0; transform: scale(0.5); }
15% { offset-distance: 15%; opacity: 1; transform: scale(1); }
85% { offset-distance: 85%; opacity: 1; transform: scale(1); }
100% { offset-distance: 100%; opacity: 0; transform: scale(0.5); }
}Worth noting: transform and offset-* both affect the element's position, but they operate in different coordinate spaces. offset-path moves the element to a point on the path, then transform: translate() applies *on top of that*. This is actually useful — you can nudge an element perpendicular to the path without recalculating path coordinates. Think of it like a train on tracks where transform lets you lean out the window.
If you want to trigger motion path animations on scroll rather than on a timer, the Intersection Observer API is your friend. Add a class when the element enters the viewport, which kicks off the animation. Or — if you're on Chrome 115+ — pair offset-distance with CSS scroll-driven animations using animation-timeline: scroll() and animate the dot as the user scrolls down the page. That's zero JavaScript for a parallax-style effect.
SVG Path Syntax Cheatsheet for Motion Path
You don't need to memorize all SVG path commands to use Motion Path, but knowing the five most useful ones saves a lot of trial and error. M x y moves the pen without drawing (start point). L x y draws a straight line. C x1 y1 x2 y2 x y draws a cubic bezier — the one you'll use most for smooth curves. A rx ry x-rotation large-arc sweep x y draws an arc, which is how you make circular orbits. Z closes the path back to the start.
For a circle orbit, the cleanest SVG path is actually two arcs that together form a full circle. A perfect circle in a single A command is mathematically impossible (an arc can't span 360°), so you split it:
``css
/* Circle orbit centered at 150,150 with radius 100 */
.planet {
offset-path: path(
'M 250 150 A 100 100 0 1 1 249.99 150'
);
animation: orbit 5s linear infinite;
}
@keyframes orbit {
to { offset-distance: 100%; }
}
``
Look, if you're doing anything complex — figure-eights, brand logo paths, custom wave shapes — just draw it in Figma, Inkscape, or Illustrator, export the SVG, and copy the d attribute. Don't hand-write cubic beziers for anything beyond simple curves. Life is too short. Then paste it into your CSS path() call and you're done.
For generative path animations — like particles following random Lissajous curves — you'll want JavaScript to write the offset-path string dynamically. That's completely fine; the property is settable via element.style.offsetPath = 'path("...")'. Generate your paths in JS, let CSS handle the actual animation. That hybrid approach is often cleaner than a full JavaScript animation loop since the browser can optimize CSS keyframe animations off the main thread.
Performance, Accessibility, and Browser Quirks
offset-distance is a compositable CSS property — meaning the browser can animate it on the compositor thread, entirely off the main JavaScript thread. This gives you 60fps (or 120fps on ProMotion displays) without blocking UI interactions. It's the same performance tier as transform and opacity. For comparison, animating left/top or margin forces layout recalculations every frame and will tank your Lighthouse score.
That said, the path *computation* itself isn't free. Very long or highly complex SVG paths (thousands of control points) can cause initial paint overhead. Keep path strings reasonable — if you're animating a logo with 200 bezier segments, consider simplifying the path in Inkscape with Path > Simplify before pasting it into CSS. Reducing a 3,400-character path string to 400 characters usually has zero visible impact at animation speeds.
Accessibility: pure decorative path animations should have aria-hidden="true" on the animated element. If the animation conveys meaning (a progress indicator, a directional cue), it needs a text alternative. Always wrap motion animations in a prefers-reduced-motion check. The reduced-motion version doesn't have to be static — you can offer a simpler fade or no animation at all:
``css
@media (prefers-reduced-motion: reduce) {
.dot {
animation: none;
offset-distance: 50%; /* park it at the midpoint */
}
}
``
Firefox has one known quirk as of 2026: offset-path: url(#id) referencing an SVG in the same document works, but if the referenced SVG is in a <defs> block inside a shadow root, Firefox won't resolve it. Inline path() strings sidestep this entirely. Safari 16–17 had a bug where offset-rotate: auto didn't update correctly on animation-direction: reverse — that's been fixed in Safari 18, but if you're supporting Safari 16 specifically, set an explicit fixed angle for reversed animations.
If you're building UI components that ship these animations — say, a notification badge that orbits a button, or a loading indicator — check out the Empire UI component library. Several of the animated components there already use Motion Path under the hood, so you get the performance benefits without managing the CSS math yourself. Same goes for the aurora and cyberpunk style families, which pair beautifully with path-based particle effects.
Real-World Patterns: When to Reach for Motion Path
Orbital UI — a secondary icon, badge, or indicator that rotates around a primary element — is the most common real-world use case. Think "verified" checkmarks orbiting avatars, live status indicators circling a video thumbnail, or a "sale" badge that slowly revolves around a product card. Motion Path handles all of these in ~20 lines of CSS with no layout impact whatsoever.
Progress indicators are another killer use case. Instead of a linear progress bar, imagine a circular indicator where a glowing dot travels along the arc proportional to completion percentage. You'd drive offset-distance with a CSS custom property set from JavaScript: element.style.setProperty('--progress', progress + '%') — then in CSS: offset-distance: var(--progress). Dead simple, and the animation between values is automatically interpolated.
.progress-dot {
offset-path: path('M 90 50 A 40 40 0 1 1 89.99 50');
offset-distance: var(--progress, 0%);
transition: offset-distance 0.4s ease-out;
}For anything with a strong brand identity — check out the neobrutalism or vaporwave style hubs — path animations add character that a plain transition never could. A menu item that swoops in along a curved arc, a tooltip that rises from a custom bubble path — these are the details that separate a polished UI from a generic one. And since the box shadow generator and gradient generator are right there in Empire UI's toolset, you can build the full visual effect without jumping between a dozen tabs.
FAQ
Yes, as of 2026 both browsers support offset-path with SVG path() strings and url() references. Firefox added full support in version 112 (2023) and Safari in version 16. Use inline path() strings to avoid shadow DOM scoping issues in Firefox.
Both move elements along a path, but Motion Path works on any HTML or SVG element and composes cleanly with CSS keyframes, transitions, and other properties. animateMotion is SVG-only and harder to sync with the rest of your animation stack.
offset-distance is a compositor-eligible property, so yes — the browser can animate it off the main thread just like transform and opacity. It won't cause layout thrashing the way animating top/left does.
Yes. Set it via element.style.offsetDistance in JS or as a CSS custom property. For scroll-driven animations, Chrome 115+ supports animation-timeline: scroll() paired with offset-distance keyframes — no JavaScript required.