Back to Dictionary
Page & View Transitions
●●○Customize
Advanced

Liquid / Wave Transition

A full page transition that begins as a small circle at the user's click coordinate, rapidly expanding outward like a liquid ripple to overtake the screen and reveal the new view.

Use

Entering highly immersive states (e.g., full-screen image viewer, games)

Avoid

Fast, utilitarian navigation (e.g., switching simple tabs)

Safer Alternative

No safer default listed.

Risk Level

Low risk

Live Demo

A

Light View

Click anywhere to trigger the wave.

What It Is

A full page transition that begins as a small circle at the user's click coordinate, rapidly expanding outward like a liquid ripple to overtake the screen and reveal the new view.

When to Use

  • Entering highly immersive states (e.g., full-screen image viewer, games)
  • Navigating to an opposite-themed page (Light to Dark mode switch)

When NOT to Use

  • Fast, utilitarian navigation (e.g., switching simple tabs)

Configuration Tips

  • 01Animate a CSS clip-path: circle(0% at x y) expanding to circle(150% at x y) upon click.

You've Seen It In

Material Design v3Concept appsCreative agency websites

AI Implementation Prompts

Move from a fast scaffold to production details, then tune timing and edge states.

Create a full screen overlay transition using clip-path: circle() that animates from a pinpoint to covering the entire viewport dynamically.

Reference Implementation

The same implementation approach used by the live preview, kept compact enough to inspect and adapt.

"use client";
import { motion, AnimatePresence } from 'framer-motion';
import { useState, useEffect } from 'react';

export function LiquidWaveTransitionDemo({ isPlaying = false }: { isPlaying?: boolean }) {
    const [view, setView] = useState<'A' | 'B'>('A');

    useEffect(() => {
        if (!isPlaying) {
            setView('A');
            return;
        }

        const loop = setInterval(() => {
            setView(prev => prev === 'A' ? 'B' : 'A');
        }, 3000);

        return () => clearInterval(loop);
    }, [isPlaying]);

    return (
        <div className="flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-200 relative cursor-pointer" onClick={() => setView(v => v === 'A' ? 'B' : 'A')}>

            {"text-stone-400 italic">/* View A - Light Mode Minimalist */}
            <div className="absolute inset-0 w-full h-full bg-stone-50 flex items-center justify-center flex-col z-0">
                <div className="w-16 h-16 bg-stone-200 rounded-full mb-4 animate-pulse flex items-center justify-center">
                    <span className="text-stone-400 font-bold">A</span>
                </div>
                <h2 className="text-xl font-bold text-stone-800">Light View</h2>
                <p className="text-sm text-stone-500 mt-2">Click anywhere to trigger the wave.</p>
            </div>

            {"text-stone-400 italic">/* View B - Dark Mode Premium (Clipped over View A) */}
            <AnimatePresence>
                {view === 'B' && (
                    <motion.div
                        key="viewB"
                        className="absolute inset-0 w-full h-full bg-indigo-600 flex items-center justify-center flex-col z-10"
                        "text-stone-400 italic">// The CSS circle path starts small and expands to cover 150% of the screen (guaranteeing no empty corners)
                        initial={{ clipPath: "circle(0% at 50% 50%)", opacity: 0.8 }}
                        animate={{ clipPath: "circle(150% at 50% 50%)", opacity: 1 }}
                        exit={{ clipPath: "circle(0% at 50% 50%)", transition: { duration: 0.6, ease: [0.65, 0, 0.35, 1] } }}
                        transition={{ duration: 0.8, ease: [0.33, 1, 0.68, 1] }} "text-stone-400 italic">// smooth ease out
                    >
                        <motion.div
                            initial={{ scale: 0.8, opacity: 0 }}
                            animate={{ scale: 1, opacity: 1 }}
                            transition={{ duration: 0.4, delay: 0.2 }}
                            className="w-16 h-16 bg-white/20 rounded-full mb-4 backdrop-blur-md border border-white/20 shadow-xl flex items-center justify-center text-white"
                        >
                            <span className="font-bold">B</span>
                        </motion.div>
                        <motion.h2
                            initial={{ y: 20, opacity: 0 }}
                            animate={{ y: 0, opacity: 1 }}
                            transition={{ duration: 0.4, delay: 0.3 }}
                            className="text-xl font-bold text-white tracking-tight"
                        >
                            Dark Mode Wave
                        </motion.h2>
                        <motion.p
                            initial={{ y: 20, opacity: 0 }}
                            animate={{ y: 0, opacity: 1 }}
                            transition={{ duration: 0.4, delay: 0.35 }}
                            className="text-sm text-indigo-200 mt-2"
                        >
                            Seamlessly flooding the viewport.
                        </motion.p>
                    </motion.div>
                )}
            </AnimatePresence>
        </div>
    );
}