Pourquoi votre widget de chat ignore votre bouton de mode sombre

Engineering

Vous avez livré le mode sombre. Un visiteur clique sur l’icône de lune, toute la page s’assombrit dans une transition fluide — et dans le coin inférieur droit, un rectangle blanc éclatant reste exactement comme il était. Le chat de support.

C’est un détail, et le genre de détail qu’on remarque, parce que c’est la seule partie de la page qui, de toute évidence, n’a pas été faite par vous. Voici pourquoi les widgets intégrés se trompent si souvent là-dessus, pourquoi les corrections évidentes ne marchent pas, et ce qu’un widget doit proposer pour que le vôtre ne se trompe pas.

Il y a deux modes sombres, pas un

Sur un site web, « mode sombre » désigne l’une de deux choses différentes.

  • La préférence du système d’exploitation. Le visiteur a réglé son ordinateur en sombre. Le navigateur l’expose via la media query prefers-color-scheme.

  • Le réglage de votre site. Un bouton dans votre en-tête, mémorisé dans localStorage ou dans un cookie, et le plus souvent appliqué sous forme de classe dark ou d’attribut data-theme sur <html>.

La plupart des sites dotés d’un bouton proposent trois choix — clair, sombre et système — justement parce que les deux ne sont pas toujours d’accord. Beaucoup de gens utilisent un OS en clair et veulent quand même une documentation sombre, un tableau de bord sombre, n’importe quoi en sombre le soir.

Un widget tiers voit le premier. Il ne voit pas le second, sauf si vous le lui dites.

Pourquoi « auto » ne suffit pas

Presque tous les widgets intégrables ont une option de thème, et presque tous proposent auto :

HTML
<script src="https://widget.example.com/chat.js" data-theme="auto"></script>

auto veut dire « suivre prefers-color-scheme ». C’est exactement ce qu’il faut pour un site sans bouton, et c’est pour ça que c’est une valeur par défaut raisonnable. C’est exactement ce qu’il ne faut pas dès que votre site a son propre sélecteur : un visiteur avec un OS en clair qui a choisi le sombre sur votre site obtient une page sombre et un widget clair. La media query ne voit pas la classe posée sur votre élément <html>, parce qu’elle ne regarde pas du tout votre page.

Écrire data-theme="dark" en dur ne fait que déplacer le problème vers les visiteurs qui ont choisi le clair.

Pourquoi le CSS n’y peut rien

Le réflexe suivant est d’aller le restyler de l’intérieur :

CSS
.dark .chat-panel { background: #111; color: #eee; }

Sur un widget bien construit, cela ne fait rien, et c’est voulu. Les widgets qui vivent sur les pages des autres s’affichent dans un shadow root : le CSS de la page hôte ne peut pas les casser, et le leur ne peut pas casser la page hôte. Votre sélecteur ne franchit jamais cette frontière.

Certains widgets laissent passer quelques propriétés CSS personnalisées, et il est tentant de les définir depuis votre classe de mode sombre. N’en faites rien : ces noms relèvent de la mécanique interne du widget, pas de son API. Ils changent sans la moindre entrée dans le journal des modifications, et votre mode sombre casse sans bruit à une version que vous n’avez jamais vue passer.

Pourquoi le recharger est pire

Certaines documentations de widgets proposent la manière forte : détruire le widget et le recréer avec le nouveau thème.

JavaScript
Widget.destroy();
Widget.init({ ...config, theme: 'dark' });

Ça marche, au sens où les couleurs changent. Mais tout ce que le visiteur était en train de faire est démonté. Le panneau qu’il avait ouvert se ferme. La réponse encore en cours de streaming est coupée au milieu d’une phrase. Et le visiteur clique souvent sur ce bouton justement parce qu’il lit quelque chose tard le soir — le pire moment possible pour jeter sa conversation.

Ce qu’un widget doit vraiment proposer

Le thème appartient à la page hôte, donc la page hôte doit pouvoir dire au widget quand il change. Il suffit d’une méthode :

JavaScript
KookeeChat.setTheme('dark'); // 'light', 'dark' ou 'auto'

La méthode est la partie facile. Ce qui la rend utilisable, c’est son comportement :

  1. Elle s’applique sur place. Pas de remontage : un panneau ouvert reste ouvert et une réponse en streaming continue de s’afficher. Seules les couleurs changent.

  2. Elle fonctionne avant le chargement du widget. Les scripts de widgets se chargent en async, et votre code de thème s’exécute souvent en premier. Un appel fait trop tôt doit être mis en file d’attente, pas ignoré ni transformé en erreur.

  3. L’ordre n’a pas d’importance. Que votre appel ait eu lieu avant ou après la lecture de la configuration du widget, c’est l’appel explicite qui l’emporte.

  4. auto reste une option. « Système » est un vrai choix dans votre sélecteur, et le widget doit pouvoir le suivre en direct.

Si vous évaluez un widget de chat ou de support et que votre produit a un sélecteur de thème, ces quatre points méritent d’être vérifiés avant de vous engager. C’est sur le premier que la plupart des implémentations échouent sans bruit.

Le brancher sur votre bouton

La version la plus simple se place dans la fonction que votre bouton appelle déjà :

JavaScript
function applyTheme(choice) { // 'light' | 'dark' | 'system'
  const dark = choice === 'dark' || (choice === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
  document.documentElement.classList.toggle('dark', dark);
  localStorage.setItem('theme', choice);

  KookeeChat.setTheme(choice === 'system' ? 'auto' : choice);
}

Notez que la dernière ligne traduit system en 'auto', et non en ce que « système » donne à cet instant. Passez la valeur résolue et le widget se fige à ce moment-là : quand l’OS du visiteur passe en sombre au coucher du soleil, votre page suit et le widget non. Passez 'auto' et le widget surveille lui-même l’OS.

Définir le thème de départ sans flash

setTheme() gère les changements. Pour le premier affichage, donnez au widget le choix que vous connaissez déjà, dès que votre propre script de thème s’exécute :

JavaScript
const stored = localStorage.getItem('theme'); // 'light' | 'dark' | 'system' | null
KookeeChat.setTheme(stored === 'light' || stored === 'dark' ? stored : 'auto');

Si ce code s’exécute avant le chargement du script du widget, le petit stub de file d’attente de l’extrait d’installation garde l’appel jusqu’à l’arrivée du script — à condition que 'setTheme' figure dans la liste de méthodes du stub. Si vous rendez la page sur le serveur et y connaissez le thème, un attribut data-theme sur la balise script fait le même travail sans aucun JavaScript.

Quand le bouton n’est pas à vous

Avec un thème Shopify, un thème WordPress ou un design system qui gère le mode sombre pour vous, il n’y a peut-être aucune fonction de bouton où ajouter une ligne. Observez plutôt le résultat :

JavaScript
const root = document.documentElement;
const sync = () => KookeeChat.setTheme(root.classList.contains('dark') ? 'dark' : 'light');

new MutationObserver(sync).observe(root, { attributes: true, attributeFilter: ['class', 'data-theme'] });
sync();

Adaptez le test à la façon dont votre thème signale le mode sombre — un attribut data-theme="dark" est aussi courant qu’une classe. Ce code passe volontairement la valeur résolue : quand vous ne voyez que le résultat, suivre exactement la page est le bon choix, et le thème résout « système » pour vous.

En React

Quand le widget est un composant React, le thème n’est qu’une prop, et il suffit de lui passer votre réglage actuel. Avec next-themes :

tsx
const { theme } = useTheme(); // 'light' | 'dark' | 'system' | undefined

<KookeeChatWidget
  clientOptions={{ apiKey }}
  theme={theme === 'light' || theme === 'dark' ? theme : 'auto'}
/>;

Utilisez theme, pas resolvedTheme. resolvedTheme est inconnu pendant le rendu serveur et au premier rendu client : un widget qui s’en sert démarre en clair et bascule un instant plus tard pour chaque visiteur en mode sombre. theme, converti de cette façon, retombe sur 'auto', ce qui est déjà correct pour la plupart d’entre eux.

Ce qu’il faut retenir

auto suit le système d’exploitation, et un bouton sur votre site est par définition quelque chose que le système d’exploitation ignore. Un widget posé sur votre page devrait prendre son thème de votre page — à l’exécution, sur place, et sans perdre ce que faisait le visiteur.


Le widget de chat IA de Kookee fonctionne déjà ainsi. Depuis la balise script, appelez KookeeChat.setTheme() ; en React, la prop theme de KookeeChatWidget se met à jour en direct. Le widget React est la seule exception à la règle du shadow root évoquée plus haut : il s’affiche dans votre page, et les variables --kookee-* qu’il lit sont documentées, vous pouvez donc les définir sans risque. Consultez Personnaliser l’apparence du widget de chat pour les options de thème, et Contrôler le widget de chat depuis JavaScript pour l’API complète.