SSGOI

How it works

The page you just left is kept alive and animated out while the new one mounts in place.

SSGOI never reads your router. It watches for elements marked with data-ssgoi-transitionappearing and disappearing from the DOM. A navigation changes a boundary's key, and SSGOI reacts to the framework destroying and rebuilding the routed node.

A four-stage route lifecycle: the old route unmounts, SSGOI restores it as an outgoing layer, both pages animate, then the outgoing layer is removed
Unmount, reinsert, animate together, clean up.
  1. Navigation changes the boundary key, so the framework unmounts the old page.
  2. SSGOI keeps that same node — never a clone — and puts it back with position: absolute, offset so it does not visually jump.
  3. The new page mounts in its normal place.
  4. The chosen transition animates both, either at once or one after the other.
  5. When the animation ends, the old node is removed for good.
Destroying the node is not the only shape this takes. With React <Activity> or Next's cacheComponents, the old page is hidden with display: none instead of being removed. SSGOI watches for that too: it reveals the same node in place, animates it, then hides it again — and the page keeps its state, so scroll position, inputs and media survive.

Everything runs through the Web Animations API: spring motion is simulated up front, turned into keyframes and handed to the browser, so there is no per-frame JavaScript once a transition is playing.

Why the wrapper needs relative and z-0

Both classes exist because of that temporarily reinserted page.

relative is the load-bearing one. An absolutely positioned element is placed against its nearest positioned ancestor; if your shell has none, the leaving page is measured against the document and lands in the wrong spot.

z-0 is about containment. Transitions stack their layers at z-index 0, 1 and 2 — never negative, so the leaving page cannot slip behind your background — but a few expressive effects go much higher. A stacking context on the wrapper keeps all of that inside your shell instead of over a fixed header.

If a transition looks broken, check the layout shell before anything else.

Which boundary owns the transition

If an outer boundary and one nested inside it both change in the same update, the outer one owns the transition. A child owns it only while its parent stays mounted.

The reason is mechanical: the outer node is the one the framework removes, and the inner boundary goes with it. SSGOI still cleans the inner one up, but there is no separately removed node left to animate, so the outer boundary's transition is the only one that runs.

tab → tab
app-shell stays ── main-content changes ── BottomNav stays

tab → detail
app-shell changes ── nested change is absorbed ── BottomNav leaves

project child → child
project key stays ── no marked child leaves ── content swaps immediately

One consequence worth knowing: changing only data-ssgoi-transition on a node that stays mounted animates nothing. The key has to change. Persistent layouts has the full route tree, dynamic keys, and intercepted modals.