Web overlays and shared motion
Available since 0.1.0-rc.7, delivered by #320–#323. The scope and input guide covers #314–#317. Mavis adoption remains a separate integration workstream.
A rendered geometry boundary
An open modal dialog or popover participates in the browser's top layer. Its DOM ancestors can keep animating without becoming the overlay's coordinate ancestors. Jolt now stops its geometry walk at that rendered root, including Shadow DOM and pending top-layer removal. The root's own transform still matters. Identity transforms are accepted outside overlays as well.
This fixes the upstream failure reproduced by Mavis #197: an identity or moving transform on the Card containing Select must not reject the Sheet's otherwise valid drag surface. The CSS top-layer model defines this rendering distinction.
dialog.showModal() // The application owns focus, selection and dismissal.
const open = runtime.scope()
open.drag(panel, { axis: 'y', snap: { y: [0] } })
dialog.addEventListener('close', () => open.dispose(), { once: true })Ordinary nonmodal descendants of a scaled/rotated/zoomed ancestor still need a coordinate adapter; treating screen deltas as local distances would be incorrect. Jolt rejects such drag spaces before changing touch-action or attaching input. A transformed root inside the modal is also affecting geometry and remains subject to validation. Keep the drag surface's static transform baseline identity. Use a genuine top-layer modal/popover or an untransformed input surface, not a CSS z-index imitation.
Closing a dialog removes rendered surfaces from active ownership. Cancel/dispose is safe even immediately after closing; recreate its input attachments on reopening. This does not change Select selection, modal focus restoration, keyboard dismissal, content scrolling or the application's navigation decision. Failure leaves those host behaviors usable. FLIP and pointer-region geometry use the same rendering boundary.
Shared elements
sharedLayout() connects up to eight nonoverlapping identities inside a declared container. It captures the outgoing visual presentation, runs the application's synchronous change and waits for the destination's next layout. Vue may replace/attach the ref in the resulting microtask. The controller does not navigate or choose the content.
const shared = owner.sharedLayout(container, { transition: 'smooth' })
const detach = shared.attach(card, 'cover')
const flight = shared.transition(() => {
card.remove()
container.append(detail)
shared.attach(detail, 'cover')
})
const result = await flight.finished
// { status: 'completed' }, or an explicit cancelled/unavailable reason.Attachments can replace an identity during a transition. An old detach callback cannot remove the new attachment. Copies are inert, aria-hidden, stripped of IDs/attributes and event handlers, and have no pointer interaction. They freeze computed presentation rather than duplicate application code. Original surfaces use a private opacity contribution while their copies fly; cleanup removes only that contribution, preserving other motion and newer opacity owners. Focus remains with the application/browser.
A flight crossfades outgoing and incoming copies while translating/scaling to the destination. A reversal captures the currently presented copies before cancelling the old flight. Content is visually scaled; this is different from real text reflow. cancel()/stop() resolve with a reason, preserve the changed application layout and remove all copies. pause()/suspend() cancel the current flight and block new transitions until play()/resume(). Container/target removal, scroll/resize changes and live reduced motion also clean up. Reduced motion still runs the application mutation and leaves final content visible.
layoutTimeout defaults to 500 ms (maximum 10 seconds); absent and zero-size destinations report missing-target or layout-timeout. maxElements defaults to 512 copied elements across the operation, including reversal captures (maximum 4096). The result may be unsupported-target: static HTML/text and decoded images are supported; form controls, canvas/video, embedded documents, custom elements, Shadow DOM content, generated pseudo-content and rotated/skewed/projective snapshot roots need another capture strategy. Positive axis-aligned geometry is supported. Unsupported capture still runs the synchronous application change and leaves its real content visible. A thrown application mutation cleans up and propagates the error.
The temporary stage uses a manual popover so it renders in viewport coordinates without inheriting a transformed page. An engine without that capability takes the unavailable path. This is a DOM presentation copy, not a screenshot or a performance claim. Applications should load/decode images and fetch data before starting the transition.
Complete interaction recipes
The executable recipe source combines three interactions in one scope:
drag()with bounded input and a spring return; independentpress()remains available.animateSize(panel, { height: open ? 'auto' : 0 })for intrinsic expansion. Application code ownsaria-expanded, inert content and focus when closing.animateLayout(list, mutation)for a keyed reorder/removal. The application owns the order; Jolt measures and animates the existing element identities.
Rapid toggles and reorder interruptions use existing continuity/ownership rules. There is no example-specific solver or frame loop. The returned scope disposes listeners, controllers and unfinished motion. Semantic buttons work with keyboard input; a Return button accompanies pointer dragging. These recipes complement the existing dynamic/layout APIs rather than claim virtualized drag reordering or a new component framework.
Diagnose an integration
const diagnostics = runtime.diagnostics({ development: true, maxEvents: 200 })
console.log(diagnostics.snapshot())
diagnostics.clear()
diagnostics.dispose()Snapshots report logical channels/layers, current animation/value owner IDs, retained rest channels, execution paths and active resource counts. Events identify claims, completion, takeover, explicit stops, target release, reduced motion and scope/runtime lifecycle. IDs are local to that diagnostics session; history contains numbers and enums, not DOM nodes or application values. Partial takeover leaves the untouched channels with their original owner. An animation that lost a channel can resolve false even if its remaining tracks completed.
History is bounded to 1–2000 events (default 200). Diagnostics require development: true, one diagnostic owner per runtime, and collect no history before activation or after disposal. snapshot() is pull-only; it adds no observer, timer, frame subscription or rendering work. No event callback runs inside animation transactions. Disposing the runtime retains a final serializable snapshot without retaining the document through the diagnostic handle.
A retained surface/rest channel is not an active animation. Compare active values, frame tasks, observers and scope counts against the host's baseline; the host ultimately calls release(target) or disposes its runtime. Diagnostics explain integration lifetime and ownership, not GPU execution, presented FPS or input latency.
Run npm run web:lifecycle:test and the recording workflow. The matrix includes Chromium, Firefox and WebKit, actual interaction/presentation, cleanup and packed imperative/Vue/SSR consumers. Native CI protects the existing native execution paths. The full Web/mobile parity issue #236 remains open.