offset-anchor and offset-position Explained

Part of Motion Path & Individual Transforms in Core CSS Animation Fundamentals.

The problem

A map pin should travel along a route with the tip of the pin touching the line. Instead the pin’s centre rides the line and its tip hangs a few pixels below it. A tooltip arrow meant to trace the edge of a card starts from the card’s top-left corner instead of from the point it was attached to. A set of ray() animations radiate from the wrong place — each from its own position in the layout rather than from a shared centre.

These are two different properties answering two different questions. offset-anchor answers “which point of the element is placed on the path?” offset-position answers “where does the path start, when the path itself does not say?”

Root cause analysis: two points, two coordinate systems

offset-anchor is measured in the element’s own box. offset-anchor: 50% 100% means the middle of the bottom edge; offset-anchor: 0 0 means the top-left corner. When the element is placed on the path, this point is put exactly on the path at the current offset-distance. Its initial value is auto, which uses the transform-origin value — usually the centre. (The one exception is an element with no offset-path at all but an explicit offset-position, where the anchor follows the position.) That fallback is why the pin’s centre rides the line.

offset-position is measured in the containing block. It defines the starting point for path types that do not carry one: ray(), and basic shapes such as circle() placed without an at position. path() strings do not use it — their M command already says where they start, relative to the containing block. The initial value normal means “no offset position”: rays start from the element’s own layout position, which is why items spread across a toolbar radiate from different points.

Rotation uses its own origin. offset-rotate rotates the element around the anchor point, but any additional rotate or transform still rotates around transform-origin. If those differ, combining offset motion with a hover rotation makes the element swing around an unexpected point.

Which property affects which path typepath() carries its own start; rays and shapes need offset-position.Which property affects which path typeoffset-anchor matters?offset-position matters?path('M ...')YesNo: M sets the startray(angle size)YesYes: ray origincircle() /ellipse() withoutatYesYes: shape centreinset() / rect()YesNo: uses the box
path() carries its own start; rays and shapes need offset-position.

Step-by-step resolution

Putting the right point in the right placeFix the anchor first, then the origin, then check rotation.Putting the right point in the right place1Set offset-distance: 0% and look at where the element sits.Reveals both anchor and origin errors2Set offset-anchor to the point that should touch the path.The pin's tip, the arrow's point3For ray() and shapes, set offset-position to the intended start in the containing block.Shared origin for all items4Match transform-origin to offset-anchor if the element also rotates or scales.No unexpected swing5Scrub offset-distance to 100% and check the end point.Anchor sits on the path throughout6Verify the static reduced-motion placement uses the same anchor.
Fix the anchor first, then the origin, then check rotation.

Production code pattern

/* A map pin whose TIP rides the route. */
.map {
  position: relative;
  aspect-ratio: 3 / 2;
}
.map__pin {
  position: absolute;
  inset: 0 auto auto 0;
  inline-size: 28px;
  block-size: 40px;
  offset-path: path("M 40 300 C 160 120 320 360 520 90");
  offset-anchor: 50% 100%;            /* the tip at the bottom-centre touches the line */
  transform-origin: 50% 100%;         /* hover bounce pivots on the same point */
  offset-rotate: 0deg;
  offset-distance: 0%;
  transition: offset-distance 1.8s cubic-bezier(0.45, 0, 0.25, 1);
}
.map[data-arrived] .map__pin { offset-distance: 100%; }
.map__pin:hover { scale: 1.12; }

/* An orbiting badge: a circle centred on the avatar, not on the badge. */
.avatar { position: relative; }
.avatar__badge {
  position: absolute;
  inset: 0 auto auto 0;
  offset-position: 50% 50%;           /* shape centre = centre of the avatar */
  offset-path: circle(46%);
  offset-anchor: center;
  offset-rotate: 0deg;
  animation: orbit 6s linear infinite;
}
@keyframes orbit { to { offset-distance: 100%; } }

@media (prefers-reduced-motion: reduce) {
  .map__pin { transition: none; }
  .map__pin:hover { scale: none; }
  .avatar__badge { animation: none; offset-distance: 12.5%; }  /* parked at top-right */
}

Rendering Impact: composite. Anchor and position change where the element is placed, not how it is rendered; both are resolved at style time and the per-frame work is the same offset sample as any motion path animation.

Setting transform-origin to match the anchor is the detail that keeps the hover bounce honest. Without it, scale grows the pin from its centre, and the tip lifts off the route while hovered.

The same route with two anchorsOnly the anchor differs; the path data is identical.The same route with two anchorsoffset-anchor: autoUses transform-origin: the centrePin's centre sits on the lineTip hangs 20px below the routeLooks misplacedoffset-anchor: 50% 100%Bottom-centre is the anchorTip sits on the lineHover scale pivots on the tipPin marks the route
Only the anchor differs; the path data is identical.

A debugging overlay

When positions look wrong, make both points visible. Add a temporary pseudo-element at the anchor — .map__pin::after { content: ""; position: absolute; left: 50%; top: 100%; width: 6px; height: 6px; translate: -50% -50%; background: red; border-radius: 50%; } — and draw the path itself in an inline SVG with the same d value behind the element. If the dot sits on the drawn line at every scrubbed distance, the anchor is right; if the whole route is displaced, the containing block or offset-position is wrong instead.

Verification checklist

Constraints and trade-offs

  • path() coordinates are fixed pixels in the containing block; they do not scale unless the container keeps a fixed aspect ratio.
  • offset-position support arrived later than offset-path in some engines.
  • Changing transform-origin to match the anchor also changes how unrelated transforms behave.
  • The offset shorthand can set anchor and position too, and resets any it omits.
  • Percent anchors refer to the element’s border box, so borders and padding shift the point.

Frequently asked questions

What is the difference between offset-anchor and offset-position?

offset-anchor picks a point in the element’s own box that is placed on the path. offset-position sets where rays and shapes start inside the containing block.

Does offset-position affect path()?

No. A path() string starts where its M command says, relative to the containing block. offset-position is for ray() and basic shapes without their own position.

Why does the centre of my element follow the path instead of the corner I want?

offset-anchor defaults to auto, which uses transform-origin, normally the centre. Set offset-anchor to the point you want on the path.