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.jsxfile 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
xandycoordinates 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-pathfrom 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)