You know how some websites just feel nice to use? It’s often the little things. I was on the Ant Design website the other day and noticed their theme toggle. When you click it, it doesn’t just flash — it creates this awesome ripple effect that starts right from your mouse click! I thought that was super cool and decided I had to try building it myself.
So, here’s a quick rundown of how I did it with React, Tailwind CSS, and the new View Transitions API.
Step 1: Getting the Basics Down
First things first, I needed a simple theme toggle that actually worked. I made a new component called ThemeToggle.jsx
to handle everything.
Inside, I used a useState
hook to keep track of whether the theme was 'light' or 'dark'. Then, I used a useEffect
hook to watch that state. Whenever the theme changed, it would add or remove the .dark
class from the main <html>
tag. That's how Tailwind's dark mode knows what to do!
Here’s what that first version looked like:
// In ThemeToggle.jsx
import React, { useState, useEffect } from 'react';
import { Sun, Moon } from 'lucide-react';
const ThemeToggle = () => {
// Check localStorage to see if the user had a theme set already
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
// This effect runs whenever the theme changes
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
{theme === 'light' ? <Sun /> : <Moon />}
</button>
);
};
export default ThemeToggle;ty
Step 2: Using View Transitions
Okay, so the toggle worked, but it was still just a sudden flash. This is where the View Transitions API comes in. It’s built to make changes to the page look smooth, and it’s surprisingly easy to use.
All I had to do was wrap my theme-changing line (setTheme(...)
) inside document.startViewTransition()
. Just like this:
// Inside the ThemeToggle component...
const handleThemeToggle = (event) => {
const newTheme = theme === 'light' ? 'dark' : 'light';
// If the browser is old and doesn't support the API, just do it the old way
if (!document.startViewTransition) {
setTheme(newTheme);
return;
}
// Wrap our state change in the transition function!
document.startViewTransition(() => {
setTheme(newTheme);
});
};
Just by adding that, the ugly flash was gone! The browser automatically replaced it with a nice, gentle fade. It was already a huge improvement, but I still wanted that ripple.
Step 3: Making the Ripple!
To get the ripple effect, we need to handle the animation.
-
Turn Off the Default Animation: The browser’s default fade animation would clash with my ripple, so I had to turn it off. I just added a small
<style>
tag in my mainApp.jsx
file to do this. Putting it there makes sure it's loaded and ready to go from the start.
// In App.jsx
<style>
{`
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
`}
</style>
2. Create the Ripple Animation: The startViewTransition
function gives you a promise called .ready
that lets you know when it's time to start your own animation.
First, I grabbed the
x
andy
coordinates from the mouse click.Then, I figured out how big the ripple needed to be to cover the whole screen.
Finally, I used the Web Animations API (
element.animate()
) to animate aclip-path
from a tiny circle at the click point to a giant one that fills the screen. And I wrapped it inrequestAnimationFrame()
to make sure it runs super smoothly without any glitches.
Here’s the final click handler with all the logic:
// Inside ThemeToggle.jsx
const handleThemeToggle = (event) => {
const newTheme = theme === 'light' ? 'dark' : 'light';
if (!document.startViewTransition) {
setTheme(newTheme);
return;
}
const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
);
const transition = document.startViewTransition(() => {
setTheme(newTheme);
});
transition.ready.then(() => {
requestAnimationFrame(() => {
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
);
});
});
};
Final output:
And That’s It!
It’s pretty amazing how you can use a few modern web tools to turn a boring feature into a really cool interaction. The View Transitions API is super powerful, and it’s definitely worth checking out for other things, too. Hope this helps you add a little extra flair to your own projects!
Happy Coding!
Want to see a random mix of weekend projects, half-baked ideas, and the occasional useful bit of code? Feel free to follow me on Twitter! https://x.com/praveen_sripati
Top comments (0)