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.

Composing two effects on one elementSame visual result; one of them needs an extra DOM node and a shared property, the other does not.Composing two effects on one elementtransform shorthandHover and reveal both want transformSecond declaration erases the firstWorkaround: a wrapper element pereffectFunction order changes the geometryConflict resolved by DOM structureIndividual propertiesReveal owns translate, hover owns scaleNeither declaration mentions the otherNo wrapper element requiredComposition order is fixed by the specConflict resolved by the platform
Same visual result; one of them needs an extra DOM node and a shared property, the other does not.

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.

How the two systems reach one matrixIndividual properties, motion path offset and the transform shorthand are combined in a defined order every frame.How the two systems reach one matrixoffset-pathresolvedonce, at styleoffset-distancesampledper frametranslate,rotate,scaleindividuallonghandstransformshorthandappended lastOne matrixcompositedGPU
Individual properties, motion path offset and the transform shorthand are combined in a defined order every frame.

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 transform transition, 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

  1. Select the animated element and read the Computed pane. translate, rotate and scale appear as their own entries — if a value you expected is missing, a transform shorthand elsewhere is not overriding it, something reset the longhand.
  2. For a motion path, Chromium’s Elements panel renders a path overlay when offset-path is 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.
  3. Scrub the effect in the Animations panel. offset-distance is listed as an animated property with a percentage value, so a path traversal can be inspected at any point without re-triggering it.
  4. Confirm in Performance that the effect sits on the Compositor track. A path animation that appears on the Main track usually means offset-path itself is being interpolated, not offset-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.

Which property to reach forThe three columns almost never overlap in a well-structured component.Which property to reach forIndividual longhandstransform shorthandoffset-* familyComponent stateoffsetsBestConflictsWrong toolHover or focusemphasisBestWorks aloneWrong toolDecorative idleloopsWorksBestWrong toolCurved travelNeeds samplingNeeds samplingBestOrientationalong a routeManualManualoffset-rotate: auto3D perspectiveeffectsPartialBestNot applicable
The three columns almost never overlap in a well-structured component.

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.