Instead of adding document.startViewTransition
at various places in your JS, use a MutationObserver
to watch for DOM mutations. In the Observer’s callback undo the original mutation and reapply it, but this time wrapped in a View Transition.
~
# The need for automatic View Transitions
Today on BlueSky, Cory LaViska wondered the following about Same-Document View Transitions:
I wish I could opt certain elements in to View Transitions so all DOM modifications would just work without having to wrap with document.startViewTransition() 🤔
— Cory LaViska (@cory.laviska.com) November 25, 2024 at 3:11 AM
This is a very valid feature request, and also something that my colleague Adam and I have needed ourselves before. Check the following demo which triggers a View Transition in response to a radio button being checked.
See the Pen
Radiento – Bento Radio Group Carousel thing by Adam Argyle (@argyleink)
on CodePen.
In order to make it work, you need to hijack the radio selection, undo it, and then re-apply it but this time wrapped in a View Transition.
The code powering this is the following:
document.querySelectorAll('.item input').forEach(($input) => {
// @note: we listen on click because that happens *before* a change event.
// That way we can prevent the input from getting checked, and then reapply
// the selection wrapped in a call to `document.startViewTransition()`
$input.addEventListener('click', async (e) => {
if (!document.startViewTransition) return;
e.preventDefault();
document.startViewTransition(() => {
e.target.checked = true;
});
});
});
Like Cory said, it would be nice if this worked without any extra code. What if you didn’t need to hijack the click
event nor needed to riddle your JS-logic with calls to document.startViewTransition
, but had something that allows you to say: “When this changes, do it with a Same-Document View Transition”? That’d be very nice, easy, and robust.
💡 This feature is something that is on the Chrome team’s back-of-the-backlog. We are roughly thinking of a CSS property or something like that to opt-in to it, and are tentatively calling this potential feature “Declarative View Transitions”. Don’t get your hopes up for this just yet, as there are a bunch of other features – not specifically related to View Transitions – that have a much higher priority.
~
# Auto-trigger a View Transition with MutationObserver
Sparked by Cory’s request I created a POC that tries to give an answer to the problem. The starting point I used is the following demo which allows you to add and remove cards to a list.
See the Pen
Add/Remove Cards with View Transitions (using view-transition-classs) by Bramus (@bramus)
on CodePen.
Without View Transitions, the core of that demo is the following:
document.querySelector('.cards').addEventListener('click', e => {
if (e.target.classList.contains('delete-btn')) {
e.target.parentElement.remove();
}
})
document.querySelector('.add-btn').addEventListener('click', async (e) => {
const template = document.getElementById('card');
const $newCard = template.content.cloneNode(true);
$newCard.firstElementChild.style.backgroundColor = `#${ Math.floor(Math.random()*16777215).toString(16)}`;
document.querySelector('.cards').appendChild($newCard);
});
Instead of adjusting the code above to include View Transitions – as I have done in the previous embed – I resorted to adding a MutationObserver
to the code. The MutationObserver
is used to execute a callback when it observes a DOM change. In the callback I have it set to automatically undo+reapply the mutation that was done. For example, when a card gets added to the list, I immediately remove that newly added element and then re-add it wrapped in document.startViewTransition
. This works because MutationObserver callbacks are queued as microtasks, which can block rendering.
const observer = new MutationObserver(async (mutations) => {
for (const mutation of mutations) {
// A node got added
if (mutation.addedNodes.length) {
const $li = Array.from(mutation.addedNodes).find(n => n.nodeName == 'LI');
// …
// Undo the addition, and then re-added it in a VT
$li.remove();
const t = document.startViewTransition(() => {
mutation.target.insertBefore($li, mutation.nextSibling);
});
// …
}
}
});
observer.observe(document.querySelector('.cards'), {
childList: true,
characterData: false,
});
With that code in place, the snapshotting process for View Transitions is able to capture the old and new states properly when a card was added: one without the newly added element and one with the newly added element.
A similar thing is done for a card being removed: it immediately gets re-added, and only then it gets removed through a View Transition. Also in place is some logic to prevent the callback from blocking rendering indefinitely because the call $li.remove();
done in the callback would trigger a new mutation to happen.
Combined, the result is this:
See the Pen
Automatically triggered View Transitions with MutationObserver by Bramus (@bramus)
on CodePen.
~
# Not tackled by MutationObserver
Not tackled in this POC are changes like radio buttons getting checked. This because those changes are not changes that are observable by a MutationObserver
: it is a DOM property of the element that changes, not an attribute. To tackle that use-case, you can use something like my StyleObserver
to trigger an observable change when the checkbox/radio changes to :checked
. Problem there, though, is that StyleObserver
changes fire too late: because the changes fire after rendering, you get a glitch of 1 frame when reapplying the change. See the following embed, in which I adjusted Adam’s Bento demo to use @bramus/style-observer
to trigger the View Transition:
See the Pen
Radiento – Bento Radio Group Carousel thing by Bramus (@bramus)
on CodePen.
Ideally, we’d either need a StyleObserver that triggers before rendering, or something like an extension to MutationObserver
that also allows you to monitor property changes.
Also not covered are batched updates – such as elements getting removed in once place and added in a new place. In the demo above I have worked around this by manually grouping the mutations into pairs before handling them as one changeset.
~
# Spread the word
Feel free to repost one of my posts on social media to give them more reach, or link to the blogpost yourself one way or another 🙂
~
🔥 Like what you see? Want to stay in the loop? Here's how: