CSS Motion Path & Individual Transform Properties
Part of Core CSS Animation Fundamentals.
Two additions to the transform system changed what can be expressed without JavaScript: the offset-* family, which moves an element along an arbitrary path, and the individual transform properties translate, rotate and scale, which break the transform shorthand into three independently animatable slots. Both are compositor-safe, both are widely supported, and both remove a category of workaround — nested wrapper elements whose only job was to isolate one part of a transform — from production stylesheets.
The reason to care is not novelty. Every animation that composes more than one kind of movement runs into the same structural problem: transform is a single property, so two effects that both want to change it are in conflict. Wrapper elements solved that by giving each effect its own box. The individual properties solve it properly, and motion paths solve the related problem of describing curved travel without decomposing it into keyframed x and y offsets.
Concept: three slots instead of one string
transform: translateX(20px) rotate(4deg) scale(1.05) is one property holding an ordered list. Animating it means animating the whole list: a hover effect that only wants to change the scale must restate the translate and the rotate, or it will erase them. Change the list order and you change the result, because transform functions do not commute.
The individual properties split the three most common functions into their own longhands. translate: 20px 0, rotate: 4deg and scale: 1.05 are separate properties with separate values, separate transitions and separate keyframes. They compose in a fixed order — translate, then rotate, then scale — and the result is applied before any transform shorthand still present on the element. That fixed order is a feature: it removes the class of bug where two authors’ declarations produce different geometry depending on which stylesheet loaded last.
offset-path addresses a different limitation. Moving an element along a curve with transform alone means sampling the curve at author time and writing a keyframe for each sample. offset-path takes the curve itself — a path(), a ray(), a basic shape or a url() reference to an SVG path — and offset-distance moves the element along it as a single animatable percentage. One property, one keyframe pair, and the browser does the interpolation.
Execution model
Both features resolve during style and animate on the compositor, provided nothing else on the element forces a different path.
The individual properties are folded into the same transformation matrix the shorthand produces. The browser computes translate, then rotate, then scale, then appends whatever transform declares, and the result is one matrix per frame — the same amount of work the shorthand alone would have cost. Interpolation happens per property, which is exactly why they compose cleanly: two animations changing translate and scale are two independent interpolations, not one contested list.
Motion paths work slightly differently. offset-path defines a geometry; offset-distance is a length or percentage along it; offset-rotate decides whether the element turns to follow the tangent; offset-anchor decides which point of the element sits on the path. Only offset-distance (and offset-rotate, and offset-anchor) are normally animated — the path itself is static, so the browser can resolve it once and then treat the animation as a cheap position lookup. Animating offset-path itself is possible between compatible path shapes but forces the geometry to be recomputed each frame, so it belongs in the same category as animating clip-path: possible, occasionally justified, never the default.
One detail catches people out. An element with an offset-path is positioned by the path, and its own transform still applies on top. That makes it easy to combine a path traversal with a scale or a wobble — but it also means a stray transform: translate() fights the path rather than adding to it in the way you might expect. Keep positional intent in the offset properties and decorative intent in transform or the individual longhands.
Property reference
| Property | Accepted values | Compositing tier | Notes |
|---|---|---|---|
translate |
<length-percentage>{1,3} |
Composite | Independent slot; applied before rotate and scale |
rotate |
<angle>, or an axis plus an angle |
Composite | rotate: z 45deg for an explicit axis |
scale |
<number>{1,3} |
Composite | One value scales both axes; two are x then y |
transform |
Function list | Composite | Still applied, after the three longhands |
offset-path |
path(), ray(), <basic-shape>, url(), none |
Style (resolved once) | Animating the path itself forces geometry recomputation |
offset-distance |
<length-percentage> |
Composite | The property you animate; 0% to 100% traverses the path |
offset-rotate |
auto, reverse, <angle>, auto <angle> |
Composite | auto turns the element to follow the tangent |
offset-anchor |
<position>, auto |
Style | Which point of the box rides the path |
offset |
Shorthand for the five above | — | Convenient, but resets the properties it omits |
Annotated examples
Two effects, no wrapper
/* Intent: a card that rises into place on reveal and lifts on hover,
with neither effect having to know the other exists. */
.card {
translate: 0 12px; /* reveal owns the vertical slot */
scale: 1; /* hover owns the scale slot */
opacity: 0;
transition: translate 320ms cubic-bezier(0, 0, 0.2, 1),
scale 160ms cubic-bezier(0.2, 0, 0, 1),
opacity 320ms linear;
}
.card.is-revealed { translate: 0 0; opacity: 1; }
.card:hover { scale: 1.03; } /* does not disturb translate */
@media (prefers-reduced-motion: reduce) {
.card {
transition: opacity 120ms linear;
translate: 0 0; /* settle at the resting position */
scale: 1; /* no lift, state still legible */
}
}
Rendering Impact: composite only. Two transitions on two properties produce one matrix per frame — the same compositor cost as a single
transformtransition, with none of the coordination.
Travelling along a path
/* Intent: a marker that follows a curved route as the section scrolls,
turning to face its direction of travel. */
.route__marker {
offset-path: path('M 8 96 C 120 8, 260 184, 392 96');
offset-distance: 0%;
offset-rotate: auto; /* nose follows the tangent */
offset-anchor: center;
animation: travel linear both;
animation-timeline: view(); /* progress comes from scroll, not time */
animation-duration: auto;
}
@keyframes travel {
to { offset-distance: 100%; }
}
@media (prefers-reduced-motion: reduce) {
.route__marker {
animation: none;
animation-timeline: auto;
offset-distance: 100%; /* show the completed journey */
}
}
Rendering Impact:
offset-distance— composite. The path is resolved once at style time; each frame is a position lookup along a curve the browser has already flattened, and the scroll timeline means no scroll handler runs.
Keeping the shorthand for what it is good at
/* Intent: a subtle idle wobble that must not interfere with the layout
offsets applied by the component's own state classes. */
.badge {
translate: var(--badge-x, 0) var(--badge-y, 0); /* owned by state */
animation: wobble 2.4s ease-in-out infinite; /* owned by decoration */
}
@keyframes wobble {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
@media (prefers-reduced-motion: reduce) {
.badge { animation: none; transform: none; }
}
Rendering Impact: composite. The state offset and the idle animation occupy different properties, so the infinite loop can be disabled under reduced motion without touching the positioning.
DevTools workflow
- Select the animated element and read the Computed pane.
translate,rotateandscaleappear as their own entries — if a value you expected is missing, atransformshorthand elsewhere is not overriding it, something reset the longhand. - For a motion path, Chromium’s Elements panel renders a path overlay when
offset-pathis present. Hovering the declaration highlights the curve on the page, which is the fastest way to confirm the coordinate system: path coordinates are relative to the element’s containing block, not to the element itself. - Scrub the effect in the Animations panel.
offset-distanceis listed as an animated property with a percentage value, so a path traversal can be inspected at any point without re-triggering it. - Confirm in Performance that the effect sits on the Compositor track. A path animation that appears on the Main track usually means
offset-pathitself is being interpolated, notoffset-distance.
Failure modes and fixes
The element vanishes when offset-path is applied. Path coordinates are resolved against the containing block. A path written for a 400 × 200 illustration applied inside a differently sized container places the element outside the visible area. Give the container an explicit size, or express the path in percentages via a ray() or a basic shape.
transform appears to ignore the individual properties. It does not — they are composed, with transform applied last. What looks like an override is usually double-counting: the same offset expressed twice, once in translate and once in a translateY() inside the shorthand.
A hover effect resets the reveal offset. Only if both are written into transform. This is the exact problem the longhands remove; splitting the two effects across translate and scale is the fix, and it is also why animation composition is needed far less often than it used to be.
The offset shorthand wipes out offset-anchor. Like every shorthand, it resets the longhands it does not mention. Prefer the individual properties in component CSS and keep the shorthand for one-off declarations.
Rotation follows the path when it should not. offset-rotate: auto is the default in most examples but not what you want for a label that must stay upright. Set offset-rotate: 0deg explicitly.
Accessibility and reduced-motion notes
Curved travel is, by construction, larger travel: a path exists precisely because the element is going somewhere interesting. That makes motion-path animations one of the more likely candidates to trip the vestibular thresholds described in vestibular-safe motion patterns. Under prefers-reduced-motion: reduce, prefer to place the element at its final offset-distance rather than shortening the traversal — a fast trip along a curve is still a trip.
The individual properties make the reduced-motion branch cleaner than the shorthand ever allowed. Because each effect owns its own property, the gate can neutralise exactly one of them: keep the scale feedback that tells a user their pointer is over a control, and remove the translate that carries them across the screen. With a single transform list, that surgical removal requires restating the whole list.
Finally, do not use offset-rotate: auto on anything containing text. Rotating text along a path is hard to read at the best of times and impossible for anyone using screen magnification, which does not rotate with it.
Frequently asked questions
Do the individual transform properties replace the transform shorthand?
No, they compose with it. The browser applies translate, then rotate, then scale, then whatever the transform shorthand declares. Use the longhands for values that different effects need to own independently, and keep the shorthand for decorative motion or 3D functions that have no longhand.
Is animating offset-distance as cheap as animating transform?
Effectively yes. The path is resolved once during style, so each frame is a lookup along a flattened curve followed by the same matrix composition a transform animation performs. Animating offset-path itself is the expensive case, because the geometry has to be recomputed.
Why is my element positioned somewhere unexpected once offset-path is set?
Path coordinates resolve against the element’s containing block, not against the element. A path authored for one illustration size will land somewhere else inside a differently sized container. Give the container explicit dimensions, or use ray() or a basic shape so the geometry scales with the box.
Can I transition between two different offset-path values?
Only between compatible path definitions, and it forces the geometry to be recalculated each frame. Treat it like animating clip-path: occasionally worth it for a deliberate effect, never the default. Swapping paths at a state boundary while offset-distance holds still is usually the better design.
Which of these need a fallback in older browsers?
Both features are supported across current engines, but a stale browser that ignores translate, rotate or scale will render the element at its untransformed position — which is the correct resting state if you author the base styles that way. Never encode the visible state solely in the animated property.
Related
- Hardware-Accelerated Properties — why the whole transform family stays on the compositor
- CSS Animation Composition & Layering — the other answer to two effects on one property
- Keyframe Architecture & State Mapping — driving these properties from a single state attribute
- Scroll-Driven Animation Patterns — the timeline used to advance
offset-distancewith scroll - Vestibular-Safe Motion Patterns — distance and velocity limits that apply to path travel
- Animating an Element Along an Offset Path — coordinate systems, orientation and scroll-driven traversal
- Individual Transform Properties vs the transform Shorthand — giving each effect its own slot instead of a wrapper element