La scène .th-circular-scene (300 × 220 px, coins de 16 px, overflow: hidden) empile deux calques de sa taille exacte, en position: absolute; inset: 0 : .th-circular-dark (fond #0f172a, z-index 1) et, au-dessus, .th-circular-light (fond #f8fafc, z-index 2). Chacun porte sa propre version du contenu — ici une carte avec titre, phrase et barre de progression. Le bouton .th-circular-toggle, 36 px à 12 px du haut et de la droite, est posé par-dessus les deux calques (z-index 10) : il ne bouge pas pendant la bascule, seule son icône change, soleil sur le sombre, lune sur le clair.
Au repos, le calque clair est rogné en clip-path: circle(0% at calc(100% - 30px) 30px) : un cercle de rayon nul centré exactement sur le bouton (30 px = 12 px de marge + 18 px de demi-bouton), quelle que soit la taille donnée à la scène. La classe .active le fait passer à circle(150% …) avec transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1). Mesuré 300 ms après le clic : rayon de 118,7 % dans Chromium 151, 110,7 % dans Firefox 153, 115,4 % dans WebKit 26 ; or le coin le plus éloigné du bouton est à 125 % (330 px), la scène est donc entièrement claire vers 0,35 s et la fin de la courbe ralentit hors champ. Un pourcentage de circle() se rapporte à la diagonale de la boîte divisée par √2 : depuis un point intérieur, 150 % couvre toujours tout, même si vous allongez la scène.
Le script (0,8 Ko, sans dépendance) parcourt chaque .th-circular-scene de la page : plusieurs scènes cohabitent sans id ni fonction globale. Au clic, il bascule .active, met aria-pressed à jour et alterne aria-hidden entre les deux calques, pour qu'un lecteur d'écran ne lise que le thème affiché. Retirer .active rejoue la même transition à l'envers : le cercle se referme dans le bouton (mesuré 34 à 46 % de rayon 300 ms après le second clic, 0 % à 1,5 s). Le choix des deux calques complets plutôt que d'un changement de couleurs tient au principe même : pour qu'un cercle sépare l'ancien et le nouveau thème, les deux doivent être peints en même temps. L'effet ne lit pas prefers-color-scheme et ne mémorise pas le choix : il part toujours du sombre (voir la FAQ pour l'ajouter).
CSS (clip-path: circle() animé, calc() dans la position du cercle, :focus-visible) + 0,8 Ko de JavaScript, aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26 : calques superposés au pixel, cercle centré sur le bouton, même aller-retour, mêmes touches. clip-path avec formes simples et transition existe sans préfixe depuis Chrome 55, Firefox 54 et Safari 12.1. Le plancher vient de inset: 0, qui superpose les calques : Chrome et Edge 87, Firefox 66, Safari 14.1 (iOS 14.5). :focus-visible date de Chrome 86, Firefox 85 et Safari 15.4 ; avant, le navigateur garde son contour de focus par défaut, que l'effet ne retire pas.
Sans clip-path (navigateur très ancien), le calque clair n'est jamais rogné : la scène reste claire en permanence et seule l'icône du bouton change. Sans JavaScript, la scène reste dans son thème sombre et le bouton ne fait rien ; vous pouvez poser la classe active dans le HTML pour servir directement le clair.