Flooding the page with a new theme

Most theme toggles flip the page from light to dark in a single frame, like someone switching the lights off on you. On my portfolio the new theme spreads out from the place you clicked, as a circle that grows until it fills the screen. It takes 800 milliseconds and it feels like pouring something.

Nothing is my favourite starting point.

A small page that stands in for the real one.

01 WORK
02 NOTES
03 CONTACT
Click anywhere on the little page, or press its toggle. Each click floods the other theme out from that spot.

I wrote it in August 2026 for an early draft of the site, and it went live with the current portfolio on 17 October.

View Transitions

On the real site this uses the View Transitions API on the whole document. You call document.startViewTransition() with a function that changes the theme. The browser takes a picture of the page as it was, applies the change, and then gives you two layers to animate: the old picture and the new, live page. I leave the old one alone and grow a circular clip-path on the new one, starting at the pointer.

const R = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const vt = document.startViewTransition(() => setTheme(next));
vt.ready.then(() => {
  document.documentElement.animate(
    { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${R + 60}px at ${x}px ${y}px)`] },
    { duration: 800, easing: 'cubic-bezier(.45,.05,.2,1)',
      pseudoElement: '::view-transition-new(root)' });
});

Two numbers matter. The final radius is the distance from the click to the farthest corner of the window, plus 60px, so the circle's edge is safely off screen before the animation ends, whichever corner you clicked from. And the easing, cubic-bezier(.45,.05,.2,1), starts slowly and finishes fast then soft, so the first few frames read as a drop landing rather than a wipe starting.

The body normally has a 0.6 second colour transition. During the flood I switch that off, because the new layer should arrive already finished. Otherwise you get the circle and a fade at the same time, which looks like a mistake.

The demo above can't do quite the same thing. A View Transition is for the whole document, and I didn't want this page to turn dark every time you tried it. So inside the stage I fake it: I copy the little page, give the copy the new theme, lay it on top and grow the same clip-path circle on it, with the same duration and easing. When it finishes, the real little page takes the new theme and the copy is removed. That is close to what the browser does with its two layers.

The fallback

Browsers without View Transitions get a blob. It is a div in the new background colour, 2.7 times the farthest-corner distance across, with a lopsided border radius (46% 54% 52% 48% / 44% 48% 52% 56%). It starts at a scale of 0.004 and grows to full size while turning 40 degrees, over 0.85 seconds. Then the theme swaps underneath it at 880ms and the blob fades out over half a second.

It is a cheaper trick and you can tell. The blob is only a colour, so for a moment the page underneath is still in the old theme, and the text appears with the normal 0.6 second fade once the blob goes. Pick Fallback blob above and you can see the seam.

The ink blot

Before either of those, I tried an ink blot drawn by a WebGL shader. It is still in the source, marked retired and never called. The edge of the blot is a circle pushed in and out by fractal noise, so it spreads unevenly like ink in paper, with a few fingers that reach ahead. The inside is mottled at first and evens out. At 70% of the way through, the theme swaps underneath. In the last quarter the blot dissolves through a noise threshold, so it breaks up rather than fades.

Nothing is my favourite starting point.

The same small page, flooded with ink.

01 WORK
02 NOTES
03 CONTACT
The retired WebGL ink blot, running in a box. Click anywhere on it. It takes 1.7 seconds, which is a long time to wait for a colour.

It is the prettiest of the three and the one I took out. Partly the time: 1.7 seconds, more than twice the circle. Mostly what it is: a painting of the new colour laid over the page, with the real change hidden underneath halfway through. The View Transition shows you the actual new page from the very first frame. It is simpler, and it is honest about what is happening.

With reduced motion on, both demos here swap instantly. The portfolio itself doesn't check for that yet. It should.

Built
August 2026, live October 2026
Stack
View Transitions API, WAAPI, WebGL (retired)
Lives at
thomasalex.in/portfolio
Source
inkToggle() and webglInk() in portfolio.html

Built with Claude Code. No real pages were turned dark in the making of these demos.