The scene .th-circular-scene (300 × 220 px, 16 px corners, overflow: hidden) stacks two layers of its exact size with position: absolute; inset: 0: .th-circular-dark (background #0f172a, z-index 1) and, on top, .th-circular-light (background #f8fafc, z-index 2). Each carries its own version of the content — here a card with a title, a line of text and a progress bar. The .th-circular-toggle button, 36 px, 12 px from the top and right, sits above both layers (z-index 10): it does not move during the switch, only its icon changes, sun over the dark theme, moon over the light one.
At rest, the light layer is clipped to clip-path: circle(0% at calc(100% - 30px) 30px): a zero-radius circle centered exactly on the button (30 px = 12 px margin + 18 px half-button), whatever size you give the scene. The .active class takes it to circle(150% …) with transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1). Measured 300 ms after the click: a radius of 118.7% in Chromium 151, 110.7% in Firefox 153, 115.4% in WebKit 26; the corner farthest from the button is at 125% (330 px), so the scene is fully light at about 0.35 s and the tail of the curve slows down out of view. A circle() percentage refers to the box diagonal divided by √2: from any inside point, 150% always covers everything, even if you stretch the scene.
The script (0.8 KB, no dependency) walks through every .th-circular-scene on the page: several scenes coexist with no id and no global function. On click, it toggles .active, updates aria-pressed and alternates aria-hidden between the two layers, so a screen reader only reads the theme on screen. Removing .active plays the same transition backwards: the circle closes into the button (measured 34 to 46% radius 300 ms after the second click, 0% at 1.5 s). Two full layers rather than a color change is the principle itself: for a circle to separate the old theme from the new one, both must be painted at the same time. The effect does not read prefers-color-scheme and does not remember the choice: it always starts dark (see the FAQ to add both).
CSS (animated clip-path: circle(), calc() in the circle position, :focus-visible) + 0.8 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: layers overlapping to the pixel, circle centered on the button, same round trip, same keys. Unprefixed clip-path with basic shapes and transitions exists since Chrome 55, Firefox 54 and Safari 12.1. The minimum versions come from inset: 0, which stacks the layers: Chrome and Edge 87, Firefox 66, Safari 14.1 (iOS 14.5). :focus-visible dates from Chrome 86, Firefox 85 and Safari 15.4; before that, the browser keeps its default focus ring, which the effect does not remove.
Without clip-path (a very old browser), the light layer is never clipped: the scene stays light all the time and only the button icon changes. Without JavaScript, the scene stays in its dark theme and the button does nothing; you can put the active class in the HTML to serve the light theme directly.