webdevpuneet.com

View Transitions API Dark Mode — Circular Reveal

Circular Reveal Theme Toggle (View Transitions API) · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="page" id="page">
  <header>
    <span class="logo">Acme</span>
    <button class="toggle-btn" id="toggleBtn" aria-label="Toggle dark mode">
      <svg id="sunMoon" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
        <circle cx="12" cy="12" r="5"></circle>
        <line x1="12" y1="1" x2="12" y2="3"></line>
        <line x1="12" y1="21" x2="12" y2="23"></line>
        <line x1="4.2" y1="4.2" x2="5.6" y2="5.6"></line>
        <line x1="18.4" y1="18.4" x2="19.8" y2="19.8"></line>
        <line x1="1" y1="12" x2="3" y2="12"></line>
        <line x1="21" y1="12" x2="23" y2="12"></line>
        <line x1="4.2" y1="19.8" x2="5.6" y2="18.4"></line>
        <line x1="18.4" y1="5.6" x2="19.8" y2="4.2"></line>
      </svg>
    </button>
  </header>

  <main>
    <h1>Native circular reveal</h1>
    <p>Click the sun/moon button. In Chromium browsers (Chrome, Edge, Opera) this uses <code>document.startViewTransition</code> to wipe the new theme in as an expanding circle from the button. Everywhere else it falls back to an instant, still-smooth class toggle.</p>
    <div class="cards">
      <div class="card">Card A</div>
      <div class="card">Card B</div>
      <div class="card">Card C</div>
    </div>
  </main>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real document.startViewTransition usage — not a simulated crossfade
clip-path circle() keyframe reveal expanding from the exact clicked coordinates
--x/--y CSS custom properties set from getBoundingClientRect() for a precise origin point
Explicit feature-detection fallback: unsupported browsers still switch theme, just without the wipe
Separate dark-transition/light-transition classes for independent per-direction styling
transition.finished promise used to clean up transition classes without a hardcoded timeout
Works with a single toggled .dark class — no theming library required
Honest about Chromium-only support for the native API in the documentation
Zero dependencies beyond the native browser API
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

View Transitions API Dark Mode Toggle — Circular Reveal from the Click Point

Screenshot of the Circular Reveal Theme Toggle (View Transitions API) snippet rendered live

The circular-reveal theme toggle is one of the signature demos of the native View Transitions API: click a sun/moon icon and the new theme wipes across the screen as an expanding circle centered on the button you clicked, rather than an instant flash or a plain crossfade. This snippet builds the effect with the real document.startViewTransition API and a small clip-path keyframe animation — no page framework, no transition library.

How the View Transitions API works here

Calling document.startViewTransition(callback) tells the browser: take a screenshot of the current page, run callback (which makes the actual DOM/class change), take a screenshot of the resulting new state, and then let you animate between the two screenshots with CSS. The browser exposes the old page as ::view-transition-old(root) and the new page as ::view-transition-new(root), both real pseudo-elements you can target directly in CSS. This snippet's callback is just applyTheme(goingDark), which toggles a single .dark class on #page — all of the visual choreography lives in CSS, not in the callback.

The circular clip-path reveal

The actual "circle expanding outward" look comes from a @keyframes circle-reveal animation applied to ::view-transition-new(root): it animates clip-path from circle(0% at var(--x) var(--y)) to circle(150% at var(--x) var(--y)). Because ::view-transition-new(root) is the *new* theme's screenshot sitting on top of the old one, clipping it to a growing circle makes the new theme appear to wipe in from a single point — the exact center of the button that was clicked — while the old theme stays visible everywhere the circle has not yet reached.

Positioning the circle at the click point

Before calling startViewTransition, the code reads btn.getBoundingClientRect() to find the button's center coordinates and writes them to CSS custom properties --x and --y on documentElement. The keyframe animation references var(--x, 50%) and var(--y, 50%), so the circle always originates exactly where the user clicked, with a sensible 50% 50% (page-center) fallback if the variables are ever unset.

Direction-aware transition classes

Two near-identical CSS animation triggers — .dark-transition and .light-transition — are added to documentElement just before starting the transition and removed once transition.finished resolves. Splitting them lets you give the light-to-dark and dark-to-light wipes independent timing or easing later, even though both currently share the same circle-reveal keyframes.

Honest browser support and the fallback path

The View Transitions API (document.startViewTransition) currently ships only in Chromium-based browsers — Chrome, Edge, Opera, and other Chromium derivatives. Firefox and Safari do not yet expose the function. The snippet feature-detects this with a simple if (!document.startViewTransition) guard: unsupported browsers skip straight to applyTheme(goingDark), so the theme still switches correctly everywhere, just without the circular wipe animation. This is the intended, spec-recommended pattern — treat the API as a progressive enhancement layered on top of a toggle that already works everywhere.

Reusing this pattern beyond dark mode

The same getBoundingClientRect → CSS custom property → clip-path circle-from-a-point recipe works for any full-page state change you want to "wipe in" from a specific origin — a locale switcher, an accent-color theme picker, or a full layout swap — by replacing applyTheme with whatever DOM mutation represents your new state.

Step by step

How to Use

  1. 1
    Click the sun/moon buttonIn a Chromium browser you will see the new theme expand outward from the button as a circle. In Firefox/Safari the theme switches instantly.
  2. 2
    Change the circle easing/durationEdit the 0.55s ease-in values on the circle-reveal animation in the CSS panel.
  3. 3
    Change what gets clippedSwap ::view-transition-new(root) for ::view-transition-old(root) to reveal the OLD theme shrinking away instead of the new one expanding in.
  4. 4
    Style the actual dark themeEdit the .page.dark rule and any nested dark-mode overrides to match your product's real dark palette.
  5. 5
    Add a reduced-motion guardWrap the startViewTransition call in a prefers-reduced-motion check and call applyTheme() directly for users who opt out of motion.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Site-wide dark mode toggles
A more polished alternative to the plain class-toggle in the dark mode toggle snippet, for teams that want the trending native circular-wipe effect in supporting browsers.
Theme/accent color pickers
Reuse the click-point clip-path pattern for any full-page theme change, not just light/dark — a brand accent color switcher, for example.
Learn the View Transitions API
A minimal, focused reference for document.startViewTransition, ::view-transition-old/new(root), and how CSS custom properties feed a keyframe animation origin.
Settings and preferences panels
Drop the toggle button into a settings panel as the primary appearance control.
Progressive enhancement demos
A clean, honest example of feature-detecting a cutting-edge API and providing a fully functional fallback rather than breaking in unsupported browsers.
Portfolio/demo pages
A visually striking, currently-trending effect that is inexpensive to add and demonstrates awareness of modern browser platform features.

Got questions?

Frequently Asked Questions

Chromium-based browsers — Chrome, Edge, Opera, and other Chromium derivatives — support it today. Firefox and Safari do not yet expose document.startViewTransition, so this snippet feature-detects it and falls back to an instant (but still functional) theme toggle in those browsers.

The if (!document.startViewTransition) check catches it and calls applyTheme(goingDark) directly, so the class toggle and its own CSS background/color transition still run — users just do not see the circular wipe animation.

The button element's getBoundingClientRect() gives its center coordinates, which are written to --x and --y CSS custom properties on documentElement right before startViewTransition runs. The circle-reveal keyframes read those variables as the clip-path circle() origin.

Clipping the NEW theme's screenshot to a growing circle makes the new theme appear to wipe in and cover the old one. Clipping ::view-transition-old(root) to a SHRINKING circle instead would make the old theme peel away, revealing the new theme underneath everywhere else — a different but equally valid look you can switch to.

Yes — the same document.startViewTransition wrapper works for any DOM update, including full page content swaps in an SPA. Pair it with view-transition-name on specific elements for shared-element morphs between views.

Yes. The ::view-transition-old/new(root) selectors and the circle-reveal keyframes are plain CSS and can live in a global stylesheet even if the rest of your component styling uses Tailwind utility classes.