View Transitions
this.view.transitions exposes a TransitionAPI instance – a small finite-state machine that orchestrates swaps between the current and the next view. Each entry in the set receives a transit callback together with the previous and the next values supplied to this.view.set.
At runtime you can inspect:
this.view.transitions.state—"idle" | "pending" | "running".this.view.transitions.snapshot— shape{ previous, next, startedAt, finishedAt? }.this.view.transitions.replaceWith(...)— swap the handlers in bulk.
import { State } from "@denshya/reactive"
import { Proton } from "@denshya/proton"
const screens = [
() => <section className="screen">Home</section>,
() => <section className="screen">Profile</section>,
() => <section className="screen">Settings</section>,
] satisfies Array<() => JSX.Element>
function Dashboard(this: Proton.Component) {
const index = new State(0)
this.view.transitions.clear()
this.view.transitions.add(async (transit, previous, next) => {
const previousElement = this.inflator.inflate(previous) as HTMLElement
previousElement.style.filter = "grayscale(100%)"
await Promise.resolve()
const nextElement = this.inflator.inflate(next) as HTMLElement
nextElement.style.opacity = "0"
await transit()
nextElement.style.transition = "opacity 160ms ease"
requestAnimationFrame(() => { nextElement.style.opacity = "1" })
})
if ("startViewTransition" in document) {
this.view.transitions.add(document.startViewTransition)
}
this.view.set(screens[index.get()]())
return (
<main className="dashboard">
<nav className="dashboard__actions">
<button type="button" onClick={() => index.set((index.get() + 1) % screens.length)}>
Next screen
</button>
</nav>
<div className="dashboard__stage">{this.view.current}</div>
</main>
)
}
export default Dashboard
Lifecycle
- When
this.view.setis called with a different value, the transitions state moves to pending. You can readthis.view.transitions.stateorsnapshotto inspect what is being swapped. - The queue enters the running state before the first transition handler executes.
- The previous view remains active until every handler has either called
await transit()or finished execution. Proton always calls the callback at the end of the handler to avoid deadlocks, so you can safely perform clean-ups without worrying about breaking the queue. - After the swap concludes, Proton resets the state back to idle.
Working with the View Transition API
The document.startViewTransition API can be registered as yet another handler:
if ("startViewTransition" in document) {
this.view.transitions.add(document.startViewTransition)
}
Proton invokes
document.startViewTransitionwithdocumentas the context, so you can pass the method reference without manually binding it.
The returned ViewTransition promises (ready, finished, updateCallbackDone, committed and done) are awaited before the queue settles. This allows the transition to finish fully before Proton exposes the next view.
Bulk updates
this.view.transitions is a Set. If you need to replace the whole collection you can do so via:
this.view.transitions = new Set([
customTransition,
document.startViewTransition,
])
Calling this.view.set(value) keeps the previous behaviour but now runs transitions in the background, so existing code keeps working without changes. You can also assign a brand new collection directly, as shown above.
Demo
Start from the Proton Elements template and add a src/app/ViewTransitionsDemo.tsx file that exports the Dashboard example above. Point the router to this component (e.g. render it inside App.tsx) to try the transition queue in the browser. The template already ships with the necessary styles and bundler configuration, so you only need to paste the component and hit the Run button.