theme-toggle-circular
v1.0.0
Published
Framework-agnostic circular reveal theme switcher using the View Transition API. Works with Tailwind CSS.
Maintainers
Readme
theme-toggle-circular
Circular reveal theme switcher, framework-agnostic, zero dependency.
Basé sur document.startViewTransition() + clip-path: circle() animé via la Web Animations API.
Pourquoi pas juste copier le code rdsx.dev directement ?
Le site rdsx.dev te donne le concept CSS/JS brut, mais deux problèmes en usage réel :
- Rien ne gère le sens du switch (dark→light vs light→dark). Sans ça t'as le bug que t'as eu dans ta version Vue :
light → dark → light → darkqui flicker, parce que t'animes toujours le même pseudo-élément peu importe la direction. - Rien n'est packagé — tu dois recopier la logique dans chaque projet/framework.
Ce package règle les deux : logique de direction encapsulée dans la classe, et une seule API qui marche partout.
Installation
Pas de build step nécessaire, c'est de l'ESM pur. Deux options :
# Si tu publies/utilises via npm dans un projet Vite/Webpack/etc.
npm install theme-toggle-circularou juste copie src/index.js + src/theme-toggle.css dans ton projet.
Setup obligatoire
1. Importer le CSS (une seule fois, globalement)
import "theme-toggle-circular/css";Sans ce CSS, le crossfade par défaut du navigateur rentre en conflit avec l'animation clip-path → tu vois un effet de double-image/ghosting.
2. Anti-FOUC (flash of unstyled content)
Avant que ton JS/framework hydrate, mets un script inline dans le <head> qui lit localStorage et applique la classe dark avant le premier paint :
<script>
(function () {
const stored = localStorage.getItem("theme");
const dark = stored === "dark" ||
(!stored && matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
})();
</script>C'est indépendant du package — c'est une contrainte de tout système dark-mode côté client, pas spécifique à ce repo.
Usage — Vanilla JS
import { initThemeToggle } from "theme-toggle-circular";
const toggleTheme = initThemeToggle();
button.addEventListener("click", toggleTheme);Usage — Vue 3 (Composition API)
// composables/useThemeToggle.ts
import { ThemeToggle } from "theme-toggle-circular";
import { onMounted, onUnmounted, ref } from "vue";
export function useThemeToggle() {
let instance: ThemeToggle;
const theme = ref<"dark" | "light">("light");
onMounted(() => {
instance = new ThemeToggle();
theme.value = instance.getTheme();
});
onUnmounted(() => instance?.destroy());
function toggle(event?: MouseEvent) {
instance.toggle(event);
theme.value = instance.getTheme();
}
return { theme, toggle };
}<script setup lang="ts">
import { useThemeToggle } from "@/composables/useThemeToggle";
const { toggle } = useThemeToggle();
</script>
<template>
<button @click="toggle">Toggle</button>
</template>C'est exactement le point faible que t'avais dans Dark_Theme_Toggle : là, theme.value est mis à jour après l'appel à instance.toggle(), jamais avant/pendant l'animation — donc pas de désync entre la réactivité Vue et le snapshot du navigateur.
Usage — React
import { useEffect, useRef } from "react";
import { ThemeToggle } from "theme-toggle-circular";
export function useThemeToggle() {
const ref = useRef<ThemeToggle>();
useEffect(() => {
ref.current = new ThemeToggle();
return () => ref.current?.destroy();
}, []);
return (event?: React.MouseEvent) =>
ref.current?.toggle(event?.nativeEvent as MouseEvent);
}Usage — Svelte
<script>
import { initThemeToggle } from "theme-toggle-circular";
const toggleTheme = initThemeToggle();
</script>
<button on:click={toggleTheme}>Toggle</button>Tailwind CSS
Tailwind v4 (ton cas)
Tailwind v4 utilise prefers-color-scheme par défaut pour dark:. Comme ce package pilote le thème via une classe (localStorage + toggle manuel), il faut basculer Tailwind en mode "class" avec la nouvelle syntaxe CSS-first :
/* app.css, à côté de tes @import "tailwindcss"; */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));Tailwind v3
// tailwind.config.js
export default {
darkMode: "class",
// ...
};Erreurs fréquentes
| Erreur | Cause | Fix |
|---|---|---|
| Flicker light → dark → light au clic | Toujours animer ::view-transition-new(root) peu importe la direction | Utiliser pseudoElement conditionnel selon wasDark (déjà géré dans ce package) |
| Double image / ghosting pendant l'animation | CSS theme-toggle.css pas importé | Importer le CSS globalement |
| Flash blanc/noir au chargement de page | Pas d'anti-FOUC | Script inline dans <head>, voir plus haut |
| Animation ne joue pas du tout | Navigateur sans support de startViewTransition (vieux Safari) | Le package fallback automatiquement en switch instantané, c'est normal |
| Le cercle part du mauvais endroit | Event pas transmis à toggle()/setTheme() | Toujours passer l'event du clic, sinon ça part du centre de l'écran |
| Classe dark pas prise en compte par Tailwind | darkMode pas en mode class (v3) ou @custom-variant absent (v4) | Voir section Tailwind ci-dessus |
Compatibilité navigateurs
startViewTransition est supporté sur Chrome/Edge 111+ et Safari 18+. Firefox l'a ajouté récemment (à vérifier selon ta cible). Le package dégrade proprement : sans support, le thème change instantanément sans animation — aucune erreur, aucun crash.
Structure
src/
├── index.js # classe ThemeToggle + helper initThemeToggle()
├── index.d.ts # types TS
└── theme-toggle.css # CSS obligatoire (z-index + désactivation du crossfade)
examples/
└── vanilla.html # démo autonome, ouvrable direct dans le navigateurLicense
MIT
