Recently I have been playing around with the View Transitions API which is coming to baseline browsers soon, View Transition API allows you to animate the DOM as state changes, this can include both single page changes and cross document changes.
Using the API I decided to see how it would work with something like a theme switch, that allows users to pick between light and dark theme, this functionality existed on my previous site .
Building on what I had previously and the new View Transition API, I was able to give a custom animation as the theme switched, currently I have a circle animation as it changes theme.
My site uses Astro which comes with some custom events that trigger as a page swaps, this does not effect the main parts of the View Transitions API.
Setting up the theme switch
In the main layout of my site I added a button for the user to click on.
<button id="toggle-mode" class="theme-switch-button">Toggle Theme</button>
For me I have positioned this absolute in the bottom corner, but this can be position any where on the page, and styled however needed.
Next I set up CSS variables for my colour scheme that I use on the elements across my site, first I set up the color-scheme property in my CSS that allow the CSS variables below to know which one to use based of the theme. The CSS variables below use the light-dark css function, which I pass it the brand colours and invert it based on the theme.
:root {
color-scheme: light dark;
--brand-50: light-dark(#f4f7fb, #182739);
--brand-100: light-dark(#e7eff7, #243c56);
--brand-200: light-dark(#cadced, #264666);
--brand-300: light-dark(#9cbedd, #2a527a);
--brand-400: light-dark(#679cc9, #326697);
--brand-500: light-dark(#4480b3, #4480b3);
--brand-600: light-dark(#326697, #679cc9);
--brand-700: light-dark(#2a527a, #9cbedd);
--brand-800: light-dark(#264666, #cadced);
--brand-900: light-dark(#243c56, #e7eff7);
--brand-950: light-dark(#182739, #f4f7fb);
}
In the Javascript on page load I get the HTML element, check if they is a theme existing in local storage, and default to the user preference.
const html = document.querySelector('html');
const theme = localStorage.getItem('theme') || html.style.getPropertyValue('color-scheme');
html.style.setProperty("color-scheme", theme);
For the toggle button I find the element in the DOM, add a click event listener to the button so when clicked it changes the current colour scheme to the other mode, and set the theme in local storage.
As my site uses the Astro Client Router, I had to wrap this Javascript in an event listener for the 'astro:page-load' event to make sure the theme is consistent when navigating on client side.
const toggleMode = document.getElementById('toggle-mode');
toggleMode.addEventListener('click', (e) => {
const theme = html.style.getPropertyValue('color-scheme');
html.style.setProperty("color-scheme", theme === 'light' ? 'dark' : 'light');
localStorage.setItem('theme', theme === 'light' ? 'dark' : 'light');
});
Adding View Transitions
To add the View Transition to the page on the button click we update the event listener so it adds the a view transition name of "changing-theme" to the HTML element we are animating.
We then have a if statement to check if the browser supports the API via checking 'document.startViewTransitions' exists, if it doesn't we fall back to what we had previously, if it does I write the logic to update DOM inside the same function as a callback.
toggleMode.addEventListener('click', (e) => {
html.style.viewTransitionName = 'changing-theme';
if (document.startViewTransition) {
document.startViewTransition(() => {
const theme = html.style.getPropertyValue('color-scheme')
html.style.setProperty("color-scheme", theme === 'light' ? 'dark' : 'light');
localStorage.setItem('theme', theme === 'light' ? 'dark' : 'light');
})
} else {
const theme = html.style.getPropertyValue('color-scheme');
html.style.setProperty("color-scheme", theme === 'light' ? 'dark' : 'light');
localStorage.setItem('theme', theme === 'light' ? 'dark' : 'light');
}
});
So far this will do a basic animation between the two different states, but we can customised this by creating a keyframe animation, and then applying it to the old DOM state via ::view-transition-old CSS property with the view transition name I gave to the HTML element via Javascript.
I added z-index to old element and the new element via ::view-transition-new CSS property so the old DOM was on top of the new one until fully transitioned out.
@keyframes move-out {
from {
clip-path: circle(100% at 100% 100%);
}
to {
clip-path: circle(0% at 100% 100%);
}
}
::view-transition-old(changing-theme) {
animation: 500ms cubic-bezier(0.4, 0, 0.2, 1) both move-out;
z-index: 10;
}
::view-transition-new(changing-theme) {
z-index: 9;
}
Improving the Animation
To achieve the animation eclipse animation to the area where the button was pressed I added a CSS variable to the HTML document based on where the user clicked inside the event listener.
const xPosition = `${e.clientX / window.innerWidth * 100}%`
const yPosition = `${e.clientY / window.innerHeight * 100}%`
html.style.setProperty('--theme-button-cord', `${xPosition} ${yPosition}`)
Which I then updated the CSS keyframe animation to use this variable as the center point like this:
@keyframes move-out {
from {
clip-path: circle(100% at var(--theme-button-cord));
}
to {
clip-path: circle(0% at var(--theme-button-cord));
}
}
Browser compatibility
At the current time of writing view transitions via the document start view transition API is available in chrome and safari, with just Firefox needed to become browser baseline.
Summary
The View Transition API is an incredible powerful API that allows developers to animate DOM state changes, using it for a updating the theme is just one of the example in how it can used.