Vous livrez le mode sombre. Le sélecteur est dans l'en-tête, il retient le choix, et toute l'application bascule proprement. Puis quelqu'un ouvre le widget de feedback dans le coin, et il est toujours d'un blanc aveuglant.
Ou, plus subtil : il est sombre, mais seulement parce que l'ordinateur de l'utilisateur l'est. Celui qui a choisi le mode sombre dans votre application sur une machine réglée en clair obtient un widget clair, et celui qui a choisi le clair sur une machine sombre obtient l'inverse.
Je suis tombé exactement sur ce problème dans le widget de BugDump cette semaine. Le correctif est petit, mais le raisonnement vaut pour tous les widgets tiers que vous intégrez : lanceurs de chat, boutons de feedback, bannières de cookies, bulles d'aide. Ils doivent tous répondre à la même question, quel thème dois-je prendre ?, et la plupart y répondent à partir de la mauvaise source.
Trois sources possibles pour un thème
Quand on parle du « thème de l'utilisateur », on peut désigner trois choses différentes :
La préférence du système d'exploitation. Ce que le navigateur expose via
prefers-color-scheme. Réglée une fois dans les paramètres système et partagée par tous les sites.Le réglage de l'application. Ce que l'utilisateur a choisi dans votre sélecteur : clair, sombre ou « système ». Stocké dans un cookie, dans
localStorageou dans son compte.Ce que l'application affiche réellement. En général une classe ou un attribut sur
<html>, déduit du réglage, et du système quand le réglage vaut « système ».
Votre application vit dans la deuxième et la troisième. La plupart des widgets intégrés ne connaissent que la première, parce que c'est la seule qu'ils peuvent lire sans votre aide.
Pourquoi prefers-color-scheme est le mauvais signal
Un widget qui définit son style avec une media query prefers-color-scheme fait une supposition raisonnable : la plupart des gens qui veulent le mode sombre l'ont demandé à leur système. Pour un site sans réglage de thème propre, cette supposition est juste.
Elle cesse de l'être dès que votre application a un sélecteur. La media query rapporte la préférence du système et rien d'autre. Une page ne peut pas changer ce qu'elle rapporte : votre application a beau appliquer son thème avec soin, un widget qui lit la media query n'en saura jamais rien.
Le décalage n'apparaît que chez les utilisateurs dont le réglage dans l'application diffère de celui du système, et c'est justement pour ça qu'il passe les tests : les développeurs testent en général avec les deux réglages identiques.
Pourquoi le widget ne peut pas simplement lire votre page
L'alternative évidente : le widget regarde votre page, cherche une classe dark sur <html>, la surveille avec un MutationObserver et suit le mouvement.
Cela marche pour les sites qui suivent exactement cette convention, et échoue sans bruit pour tous les autres. Avec la stratégie par classe de Tailwind, dark se trouve sur l'élément racine. D'autres sites utilisent data-theme="dark", un attribut data-mode, une classe sur <body>, ou un jeu de propriétés CSS personnalisées sans aucun marqueur. Certains s'affichent en sombre par défaut et marquent le clair. Un widget qui devine se trompera pour quelqu'un, d'une manière difficile à diagnostiquer pour cette personne.
La page connaît son propre thème. La solution fiable, c'est que la page le dise.
Un contrat en deux parties qui fonctionne
Ce que j'ai retenu, et ce que je conseillerais à quiconque construit un widget intégrable, tient en deux parties.
1. Un thème de départ, déclaré d'emblée. Le code d'intégration accepte le thème en configuration :
<script
src="https://bugdump.com/sdk/latest.js"
async
data-api-key="your-api-key"
data-theme="dark"
></script>
Si votre serveur connaît le choix de l'utilisateur, écrivez-le directement dans la balise. Le widget a alors les bonnes couleurs dès son tout premier affichage, avant que la moindre ligne de votre JavaScript ne s'exécute. Un réglage « système » dans l'application correspond à auto, qui rend la décision au système d'exploitation : le seul cas où la media query est le bon signal.
2. Un setter pour chaque changement ensuite.
bugdump('setTheme', 'dark'); // 'light', 'dark' or 'auto'
Appelez-le depuis votre sélecteur. Le widget bascule immédiatement, y compris un panneau déjà ouvert.
Aucune des deux parties ne suffit seule. Une configuration sans setter, c'est ce que je livrais avant : correcte au chargement, périmée après le premier changement jusqu'au rechargement de la page. Un setter sans configuration, c'est un widget qui s'affiche dans un thème par défaut, puis change de thème sous les yeux de l'utilisateur quand votre code le rattrape.
Le brancher dans un framework
En React, un composant qui n'affiche rien fait l'affaire :
import { useEffect } from 'react';
export function WidgetThemeSync({ theme }: { theme: 'light' | 'dark' | 'system' }) {
useEffect(() => {
window.bugdump?.('setTheme', theme === 'system' ? 'auto' : theme);
}, [theme]);
return null;
}
Deux détails font la différence entre « ça marche » et « ça marche presque ».
Synchronisez le réglage, pas la couleur résolue. Beaucoup de fournisseurs de thème exposent les deux : le choix de l'utilisateur (light, dark, system) et la couleur appliquée en ce moment. La valeur appliquée est souvent calculée après le premier rendu, si bien qu'elle peut contenir un instant une valeur provisoire, en général le thème clair. La synchroniser pousse cette valeur provisoire dans le widget, qui passe un instant en clair pour chaque utilisateur en mode sombre, à chaque chargement de page. Le réglage, lui, est correct dès le premier rendu.
Laissez « système » rester « système ». On est tenté de le résoudre soi-même et de passer light ou dark. Passez plutôt auto : le widget suit alors seul les changements du système, sans aucun câblage d'événements de votre côté.
Notes pour les auteurs de widgets
Si vous êtes de l'autre côté, à construire quelque chose que d'autres intègrent, voici les choix qui m'ont simplifié l'implémentation.
Mettez le thème sur un seul élément et laissez CSS faire le reste. La palette de mon widget est un jeu de propriétés CSS personnalisées définies sur le shadow host, et le thème est une classe sur ce host. Changer de thème revient à échanger une classe : pas de nouveau rendu, pas d'état perdu, et un panneau ouvert se met à jour sur place.
Vérifiez chaque racine que vous montez. Mon éditeur d'annotation de captures d'écran était monté à part, directement sur document.body, avec sa propre shadow root et sa propre copie de la palette. Il ne voyait jamais la classe de thème du host et continuait discrètement de suivre le système. Personne ne l'a remarqué tant que tout suivait le système ; dès que les thèmes explicites ont fonctionné, il est devenu la seule surface claire dans une interface sombre. Si votre widget monte des modales, des infobulles ou des overlays hors de sa racine principale, chacun d'eux a aussi besoin du thème.
Faites en sorte que le setter puisse être appelé tôt. Les intégrations se chargent en général en async, si bien que le premier appel setTheme d'une page peut facilement arriver avant le script. Un petit stub de file de commandes, le motif qu'utilisent les snippets d'analytics depuis des années, garde les appels jusqu'au démarrage du SDK, puis les rejoue dans l'ordre.
Ne faites pas de la réinitialisation la réponse. « Détruire et réinitialiser avec de nouvelles options » gère techniquement les changements à l'exécution, mais jette tout ce que le widget conservait : logs en mémoire tampon, formulaire ouvert, message à moitié écrit. Le thème est l'option que les utilisateurs changent le plus souvent pendant qu'une page est ouverte. Il mérite un setter.
Comment c'est fait dans BugDump
Depuis le SDK 0.0.39, le widget prend un thème de départ via theme ou data-theme et le change sur place via setTheme(), depuis la file de commandes de la balise script ou sur l'instance npm. L'éditeur de captures d'écran suit le même thème.
La note du journal des modifications donne les détails, et Adapter le widget au thème de votre application propose des exemples à copier-coller pour React, Vue et JavaScript pur.
Pour aller plus loin
Localiser un widget intégré : langue de la page ou du navigateur ? — la même question que pour le thème, côté langue : quel signal un widget intégré doit suivre.
Comment ajouter un bouton de signalement de bug à votre site — installer le widget sur un site, de la balise script à la première remontée de bug.
Les trois quarts de mon SDK étaient du code que presque personne n'exécutait — comment le SDK du widget a perdu 76 % de son poids.