DEV Community

Praveen Sripati
Praveen Sripati

Posted on

I Stole This Awesome Ripple Effect from Ant Design (And You Can Too!).

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
Enter fullscreen mode Exit fullscreen mode

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);
  });
};
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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 main App.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>
Enter fullscreen mode Exit fullscreen mode

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 and y 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 a clip-path from a tiny circle at the click point to a giant one that fills the screen. And I wrapped it in requestAnimationFrame() 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)',
        }
      );
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

Final output:

Final Output Gif

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)