How to Animate a Dark Mode Theme Transition
Techniques for smooth dark mode theme transitions: global CSS color crossfades, circular ripple reveals using the View Transitions API, and icon morphing.
Core Takeaway
Animating dark mode requires balancing visual smoothness with performance. For a simple site, a 200ms CSS transition on background-color and color is sufficient. For a modern app, use the View Transitions API with a circular clip-path expanding outward from the toggle button, paired with an icon morph from sun to moon.
The pitfall of universal transition: all
Applying transition: all 300ms to every DOM element causes severe performance lag during theme changes because the browser recalculates layout for hundreds of nodes simultaneously. Only transition background-color, border-color, and color on container elements.
The View Transitions API circular reveal
The modern standard for dark mode animations is document.startViewTransition(). By combining it with a circular clip-path starting from the cursor or toggle switch, you create a ripple reveal that illuminates or darkens the entire interface in a single compositor pass.
- Start theme update inside document.startViewTransition()
- Calculate click coordinates (x, y) to originate the clip-path circle
- Animate clip-path: circle(0px) to circle(maxRadius)
- Morph toggle icon from sun rays to moon crescent using SVG path interpolation
Handling local storage and flash of unstyled theme
Never animate the theme transition during the initial page load. Read the stored theme synchronously in an inline head script so the page renders in the correct theme on frame 0, and only enable transitions for user-initiated clicks.
Accessibility and photosensitivity
Sudden full-screen brightness shifts can trigger ocular strain or seizures. Keep the transition duration moderate (250-400ms) and skip the circular reveal when prefers-reduced-motion is active.
Production Implementation Snippet
/* View Transitions API circular reveal */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-new(root) {
animation: reveal-theme 400ms ease-out;
}
@keyframes reveal-theme {
from { clip-path: circle(0% at var(--click-x) var(--click-y)); }
to { clip-path: circle(150% at var(--click-x) var(--click-y)); }
}Key Principles to Remember
- Never use transition: all *; target specific color properties only
- Use the View Transitions API for modern circular ripple reveals
- Prevent transition flashes on initial page render
- Provide immediate theme switching when reduced motion is requested
Frequently Asked Questions
Sources
- View Transitions API — W3C
- Smooth theme transitions with View Transitions — Chrome Developers