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.
Step-by-step resolution
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.
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-positionsupport arrived later thanoffset-pathin some engines.- Changing
transform-originto match the anchor also changes how unrelated transforms behave. - The
offsetshorthand 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.
Related
- Motion Path & Individual Transforms — the parent topic
- Animating Along a Ray with offset-path — where offset-position matters most
- offset-rotate and Auto-Orientation Along a Path — rotation around the anchor