Skip to contentWolf-Rayet

Decision records

ADR-0134 Overlays enter and leave on their grant

Accepted2026-09-24Phase 7

Gives the overlays motion, gives motion a page on the docs site, and states the one place Wolf-Rayet's motion departs from the reference it borrows its structure from.

#Context

The token package has had motion since the start: six durations, five easings, a period scale for loops, and a longest duration per attention level that reduced motion collapses to the ambient 110ms. ADR-0125 made the cascade answer to it and ADR-0127 gave loops a floor. Every component with a motion record that moves does so on its own grant.

None of them is an overlay. Popup, dropdown menu, inline dialog, tooltip, modal, blanket, drawer, flag and spotlight appeared and vanished in a single frame. The tooltip and the modal each hold a motion record and a generated transition grant, and neither read it. The docs site had no Motion page at all: an earlier one had been retired, and its address redirected to the Foundations overview.

Atlassian's motion guidance, which the site follows for structure (the Atlassian caliber direction of 2026-09-18), rests on two ideas worth taking whole. Entrances and exits are not the same motion: an entrance orients and an exit gets out of the way. And motion should use the properties the browser can run cheaply, transform and opacity, never layout.

It rests on a third idea Wolf-Rayet cannot take: that duration scales with the size of the element, so a modal takes 250ms because it is large. Here duration is attention. A surface that is only ambient may not buy a longer entrance by being big.

#Decision

Every overlay enters from where it belongs and leaves the way it came. A popup drops out of its trigger, or rises out of it when it opens above. The tooltip nudges out of its trigger. The modal and the spotlight grow from 95%. The drawer slides in from its edge. The blanket fades. The flag rises into its stack. Every entrance is a fade with at most one movement, in transform or opacity only.

Duration is the grant of the level the overlay declares. Popup and dropdown (level 2) take 155ms, drawer and spotlight (level 3) 220ms, the blanket (level 1) 110ms. The tooltip and the modal hold motion records, so they read their own tier's transition grant, which the cascade lint requires. Under reduced motion every grant is already 110ms in motion.css, so no overlay carries a reduced-motion rule of its own.

An exit is the entrance played in reverse, under a second keyframe name so the browser restarts it. Reversing the entrance mirrors its deceleration into an acceleration, which is the asymmetry the reference asks for, without a second easing token. A record-holding component cannot read --wr-easing-exit without failing the lint, and reversing its own grant is the grant.

Exits need the overlay kept on the page, so use-presence.ts holds a closing overlay mounted until its exit's animationend, with a fallback timer so none can be stranded. It starts from the open state, so a server render or a fixture draws an open overlay exactly as before. Popup, dropdown menu, tooltip, modal with its blanket, and drawer play exits. Flag and spotlight do not own their open state, so they have entrances only.

The toggle's handle moves by translate, not by its offset. It lands in the same 16px, and it no longer asks the browser to lay out the page on every frame.

The docs site gains Foundations → Motion and Motion → Applying motion, and the rail can draw a page under the one before it. The retired redirect from /foundations/motion is removed.

#Consequences

No budget value moved. The engine renders with reduced motion and captures with animations disabled, so every overlay is measured in its final frame, as it was. Fifty four fixtures carry the new stylesheet text and were regenerated. The engine did read the DOM a frame before the screenshot, though, and caught the modal at 97.7% of its size in the receipts, so the render normalisation in packages/budget/src/config.js now removes animation and transition as well: the geometry and the pixels are read from the same final frame, and every receipt is byte-identical to the one committed before this record. The cascade lint reports the tooltip and the modal moving on their own grants.

Two components still animate a layout property: the meter's fill moves its inline-size, and the alert's glyph moves its size and border width. Both are state changes on a component's own record rather than overlays, and moving them to transform changes how they are drawn, so they are left for their own record. They are the known exceptions to the rule Applying motion states.

packages/react/lib/Presence.js is a compiled component with no source in src. A hook first named presence.ts compiled over it on a case-insensitive disk; the hook was renamed use-presence.ts and the file restored from git before anything was committed.

#Rejected

Duration by size, as the reference does. It would give the modal 250ms and the drawer longer, and it would let a large ambient surface draw more attention than a small demand. That is the opposite of the budget.

A generated exit easing per component. The right shape if exits ever need a curve of their own, but it regenerates every tier for a difference that reversing the entrance already makes.

Entrances only. Simpler, and it leaves every overlay vanishing in one frame, which is the half of the guidance that matters most for a reader who just dismissed something.