TransitionsGratuit

Dark Mode

Le thème clair est un calque complet posé sur le sombre, découvert par un cercle qui part du centre de l'interrupteur et couvre la scène en 0,6 s ; un second geste le referme dans le bouton. CSS pour la transition, 0,8 Ko de JavaScript pour le bouton, fondu de 150 ms si l'utilisateur réduit les animations.

CSSclip-pathToggle

Mis à jour le

Dark Mode

Thème sombre actif

Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Transitions en contient 52 au total, dont 9 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

800 mSortie 14 — Orléans-Centre
87km/h
90
Arrivée 21 h 42 · 186 kmPause dans 1 h 05

① Passage en carte de nuit — GPS d'un chauffeur poids lourd

QuandAu coucher du soleil sur l'A10, le chauffeur touche le bouton en haut à droite de l'écran : la carte de jour, avec la sortie 14 à 800 m et la pause obligatoire dans 1 h 05, devient la carte de nuit.
PourquoiLe nouveau thème part du bouton touché : le chauffeur voit d'où vient le changement sans chercher, et la carte, la route et le bandeau du bas restent exactement en place, parce que les deux thèmes sont deux calques superposés au pixel.
RéglagesSens inversé : la carte de jour vit dans .th-circular-dark (calque du dessous), la carte de nuit dans .th-circular-light (calque révélé), icône lune au repos ; scène agrandie à 496 × 316 px, même tracé SVG dans les deux calques, couleurs par calque (itinéraire bleu le jour, ambre la nuit).
CARTE · 1 : 50 000
Vitesse fond6,2 nds
Route fond128°
Fond18,4 m
Port-Joinville11,8 M · 19 h 50
⟵ 1 M ⟶

② Éclairage de nuit de l'écran de navigation — traceur d'un voilier de croisière

QuandÀ la tombée de la nuit, cap sur l'île d'Yeu : l'écran clair de jour, avec la carte marine et la vitesse fond, passe en écran rouge sur noir qui préserve la vision nocturne du barreur.
PourquoiLe cercle part du bouton touché, et c'est un calque complet qui arrive : aucun élément ne repasse par le blanc pendant la bascule, ce qui compte quand l'œil est déjà adapté à l'obscurité.
RéglagesCalque révélé en #050505, traits et chiffres en rouge #ff4a3d et #7a1d16 ; scène de 496 × 316 px ; panneau de données à droite placé sous le bouton (top 58 px) pour ne pas le masquer.
LE MÉLIÈS

Les Marées d'octobre

Ven. 31 oct. · 20 h 45 · VOSTFR

Salle3
RangF
Fauteuil12

1 place · plein tarif · 9,50 €

Le Méliès · salle 3

Mode salle obscure

Une fois assis, un geste sur l'interrupteur : le billet sombre s'étend depuis le bouton, et le QR code reste lisible sur sa plaque atténuée.

Écran

③ Billet en mode salle obscure — appli d'un cinéma indépendant

QuandLe spectateur s'assoit en salle 3, rang F : il touche l'interrupteur et le billet clair affiché dans le hall devient un billet sombre qui n'éblouit plus ses voisins.
PourquoiLa révélation part du coin du téléphone où se trouve le pouce et reste douce ; le QR code garde un fond gris atténué (#bdbdc4) dans le thème sombre, donc le contrôleur peut toujours le scanner.
RéglagesScène en format téléphone, 214 × 332 px ; billet clair dans le calque du dessous, billet sombre révélé ; plaque du QR atténuée plutôt qu'inversée, textes du billet en gris #a8a29e.

Comment ça marche

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).

Accessibilité

  • Bouton : un vrai <button> nommé « Thème clair » (aria-label), avec aria-pressed à true quand le clair est affiché. Mesuré : atteint à la touche Tab dans Chromium, basculé par Entrée et par Espace dans Chromium, Firefox et WebKit ; contour violet de 2 px au focus clavier (:focus-visible).
  • Contenu en double : les deux calques portent le même texte. Le script pose aria-hidden="true" sur celui qui est caché, si bien qu'un lecteur d'écran n'annonce qu'une version. Si vous ajoutez des liens ou des champs dans les calques, rendez aussi le calque caché inerte (attribut inert), sinon la touche Tab y entrera.
  • Mouvement réduit : sous prefers-reduced-motion: reduce, plus de cercle (clip-path: none) ; le calque clair apparaît en fondu d'opacité de 150 ms. Mesuré : opacité 0,39 (Chromium) et 0,33 (Firefox) 60 ms après le clic, 1 à 300 ms.
  • Contraste : sur le sombre, titre blanc à 15,7:1 et phrase à 50 % de blanc à 4,97:1 ; sur le clair, titre #0f172a à 17,1:1 et phrase #64748b à 4,6:1. Gardez ces deux couleurs secondaires si vous changez les fonds.

Compatibilité navigateur

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.

Chrome 87+✓ Complet
Firefox 66+✓ Complet
Safari 14.1+✓ Complet
Edge 87+✓ Complet
Mobile iOS 14.5+✓ Complet
Android Chrome 87+✓ Complet

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.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="th-circular-root">
  <div class="th-circular-scene">
    <button class="th-circular-toggle" type="button" aria-pressed="false" aria-label="Thème clair">
      <svg class="th-circular-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
      <svg class="th-circular-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
    </button>
    <div class="th-circular-dark">
      <div class="th-circular-card">
        <h3>Dark Mode</h3>
        <p>Thème sombre actif</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
    <div class="th-circular-light" aria-hidden="true">
      <div class="th-circular-card">
        <h3>Light Mode</h3>
        <p>Thème clair actif</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
  </div>
</div>
CSS
.th-circular-root,
.th-circular-root *,
.th-circular-root *::before,
.th-circular-root *::after {
  box-sizing: border-box
}

.th-circular-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.th-circular-scene {
  width: 300px;
  height: 220px;
  position: relative;
  border-radius: 16px;
  overflow: hidden;
}

/* Les deux thèmes sont deux calques superposés de la taille de la scène */
.th-circular-dark,
.th-circular-light {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.th-circular-dark {
  background: #0f172a;
  z-index: 1;
}

/* Le clair attend, réduit à un cercle de rayon 0 centré sur le bouton (30 px du bord haut et du bord droit) */
.th-circular-light {
  background: #f8fafc;
  z-index: 2;
  clip-path: circle(0% at calc(100% - 30px) 30px);
  transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

.th-circular-scene.active .th-circular-light {
  clip-path: circle(150% at calc(100% - 30px) 30px);
}

.th-circular-dark h3 {
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-dark p {
  color: rgba(255, 255, 255, 0.5);
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-light h3 {
  color: #0f172a;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-light p {
  color: #64748b;
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  background: rgba(255, 255, 255, 0.1);
  transition: background-color 0.3s ease;
}

.th-circular-scene.active .th-circular-toggle {
  background: rgba(0, 0, 0, 0.08);
}

.th-circular-toggle:focus-visible {
  outline: 2px solid #a78bfa;
  outline-offset: 2px;
}

.th-circular-toggle svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
  fill: none;
}

.th-circular-sun {
  stroke: #fbbf24;
}

.th-circular-moon {
  stroke: #6366f1;
  display: none;
}

.th-circular-scene.active .th-circular-sun {
  display: none;
}

.th-circular-scene.active .th-circular-moon {
  display: block;
}

.th-circular-card {
  width: 220px;
  padding: 16px;
  border-radius: 12px;
}

.th-circular-dark .th-circular-card {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card {
  background: #fff;
  border: 1px solid #e2e8f0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.th-circular-card-bar {
  height: 6px;
  border-radius: 3px;
  margin-top: 10px;
}

.th-circular-dark .th-circular-card-bar {
  background: rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card-bar {
  background: #e2e8f0;
}

.th-circular-card-bar-fill {
  height: 100%;
  width: 65%;
  border-radius: 3px;
  background: linear-gradient(90deg, #8b5cf6, #a78bfa);
}

/* Mouvement réduit : pas de cercle qui grandit, un fondu de 150 ms */
@media (prefers-reduced-motion: reduce) {
  .th-circular-light,
  .th-circular-scene.active .th-circular-light {
    clip-path: none;
  }
  .th-circular-light {
    opacity: 0;
    transition: opacity 0.15s linear;
  }
  .th-circular-scene.active .th-circular-light {
    opacity: 1;
  }
}
JavaScript (fx-0705)
(function () {
  /* fx-0705 Dark Mode : le bouton de chaque .th-circular-scene pose ou retire .active (le CSS fait la transition) */
  document.querySelectorAll('.th-circular-scene').forEach(function (scene) {
    var btn = scene.querySelector('.th-circular-toggle');
    var dark = scene.querySelector('.th-circular-dark');
    var light = scene.querySelector('.th-circular-light');
    if (!btn) return;
    function sync() {
      var on = scene.classList.contains('active');
      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
      if (dark) dark.setAttribute('aria-hidden', on ? 'true' : 'false');
      if (light) light.setAttribute('aria-hidden', on ? 'false' : 'true');
    }
    btn.addEventListener('click', function () {
      scene.classList.toggle('active');
      sync();
    });
    sync();
  });
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Durée et courbe (CSS — transition de .th-circular-light) 0.6s cubic-bezier(0.4, 0, 0.2, 1) La scène est couverte vers 0,35 s, le reste de la courbe se joue hors champ. 0,9 s donne une bascule plus solennelle ; en dessous de 0,4 s, l'œil ne voit plus le cercle partir du bouton.
Point de départ (CSS — at calc(100% - 30px) 30px) centre du bouton : 30 px du bord droit, 30 px du haut Si vous déplacez le bouton, changez cette position dans les deux règles (repos et .active) : à gauche, at 30px 30px ; au centre, at 50% 50%. Le cercle doit toujours sortir de l'élément touché.
Rayon final (CSS — circle(150% …)) 150 % Depuis n'importe quel point intérieur, 141 % suffit toujours (le pourcentage se rapporte à la diagonale divisée par √2). Le baisser à 125 % raccourcit la partie invisible de la transition sans découvrir de coin.
Couleurs des deux thèmes (CSS — background de .th-circular-dark et .th-circular-light, puis leurs h3, p, cartes) #0f172a et #f8fafc Chaque calque a ses propres règles de texte : changez-les ensemble pour garder au moins 4,5:1 sur la phrase secondaire.
Sens de la bascule (HTML — contenu des deux calques) sombre dessous, clair révélé Pour passer du clair au sombre, mettez la version claire dans le calque du dessous et la sombre dans le calque révélé, et échangez les deux icônes du bouton : le cercle découvre alors la nuit.
Mouvement réduit (CSS — bloc @media (prefers-reduced-motion: reduce)) fondu d'opacité de 150 ms Mettez transition: none pour un passage instantané ; ne remettez pas le cercle dans ce bloc.

FAQ

L'effet suit-il le thème du système et retient-il le choix du visiteur ?

Non. Il part toujours du thème sombre, ne lit pas prefers-color-scheme et n'enregistre rien. Pour les deux, quelques lignes suffisent dans le script, avant le premier affichage : var choix = localStorage.getItem('theme'); if (choix ? choix === 'light' : matchMedia('(prefers-color-scheme: light)').matches) { scene.classList.add('active'); sync(); }, puis localStorage.setItem('theme', scene.classList.contains('active') ? 'light' : 'dark') dans le clic, après sync(). Posez la classe avant que la scène s'affiche (dans le HTML côté serveur, ou avec transition: none le temps du chargement), sinon le cercle se joue à l'ouverture de la page.

Pourquoi deux calques complets plutôt qu'un changement de couleurs ?

Parce qu'un cercle qui sépare l'ancien et le nouveau thème demande que les deux soient peints en même temps : le calque du dessus est simplement rogné. Le prix est un contenu en double, que le script masque aux lecteurs d'écran avec aria-hidden. Pour une page entière, ce doublement devient lourd : l'API View Transitions (voir View Transitions dans cette catégorie) capture l'ancienne page en image et peut jouer le même cercle sur la page réelle, sans dupliquer le DOM.

Dark Mode ou Wipe Transition (fx-0707) pour un interrupteur de thème ?

Les deux superposent deux calques complets et en rognent un. Ici le nouveau thème grandit en cercle depuis le bouton, ce qui relie le geste au changement ; Wipe Transition le fait avancer de gauche à droite avec un trait lumineux sur la frontière, ce qui montre l'avant et l'après côte à côte pendant 0,6 s. Choisissez Dark Mode quand l'interrupteur est visible et bien placé, Wipe Transition quand la bascule n'a pas de point d'origine (un réglage dans un menu, une heure programmée).