Você lançou o modo escuro. Um visitante clica no ícone da lua, a página inteira escurece numa transição suave — e, no canto inferior direito, um retângulo branco e brilhante continua exatamente onde estava. O chat de suporte.
É um detalhe, e é o tipo de detalhe que as pessoas percebem, porque é a única parte da página que claramente não foi feita por você. A seguir: por que os widgets incorporados erram isso com tanta frequência, por que as correções óbvias não funcionam e o que um widget precisa oferecer para que o seu não erre.
Existem dois modos escuros, não um
“Modo escuro” num site quer dizer uma de duas coisas diferentes.
A preferência do sistema operacional. O visitante deixou o notebook no modo escuro. O navegador expõe isso pela media query
prefers-color-scheme.A configuração do seu site. Um seletor no cabeçalho; a escolha fica guardada em
localStorageou num cookie e normalmente é aplicada como uma classedarkou um atributodata-themeno<html>.
A maioria dos sites com esse seletor oferece três opções — claro, escuro e sistema — justamente porque os dois discordam. Muita gente usa o sistema no modo claro e mesmo assim quer uma documentação escura, um painel escuro, qualquer coisa escura à noite.
Um widget de terceiros enxerga o primeiro. O segundo, ele só enxerga se você contar.
Por que “auto” não basta
Quase todo widget incorporável tem uma opção de tema, e quase todos oferecem auto:
<script src="https://widget.example.com/chat.js" data-theme="auto"></script>
auto quer dizer “siga o prefers-color-scheme”. Isso é exatamente o certo para um site sem seletor de tema, e por isso é um padrão sensato. E é exatamente o errado a partir do momento em que o seu site tem o próprio seletor: um visitante com o sistema no modo claro que escolheu o escuro no seu site recebe uma página escura e um widget claro. A media query não enxerga a classe no seu elemento <html>, porque ela nem olha para a sua página.
Fixar data-theme="dark" no código só transfere o problema para os visitantes que escolheram o claro.
Por que não dá para resolver com CSS
O próximo instinto é entrar no widget e mudar o estilo dele:
.dark .chat-panel { background: #111; color: #eee; }
Num widget bem feito, isso não faz nada, e é de propósito. Widgets que vivem nas páginas dos outros renderizam dentro de um shadow root, para que o CSS da página hospedeira não os quebre e o deles não quebre a página. O seu seletor nunca atravessa essa fronteira.
Alguns widgets deixam passar algumas custom properties de CSS, e dá vontade de defini-las a partir da sua classe de modo escuro. Resista: esses nomes são detalhes internos do widget, não a API dele. Eles mudam sem nenhuma entrada no registro de alterações, e o seu modo escuro quebra em silêncio numa versão que você nem viu.
Por que recarregar é pior
A documentação de alguns widgets sugere a solução bruta: destruir o widget e criá-lo de novo com o tema novo.
Widget.destroy();
Widget.init({ ...config, theme: 'dark' });
Funciona, no sentido de que as cores mudam. Mas também desmonta tudo o que o visitante estava fazendo. O painel que estava aberto fecha. A resposta que ainda chegava em streaming é cortada no meio da frase. E muitas vezes o visitante troca o tema justamente porque está lendo algo tarde da noite — o pior momento possível para jogar a conversa dele fora.
O que um widget precisa oferecer
O tema pertence à página hospedeira, então ela precisa conseguir avisar o widget quando o tema muda. Basta um método:
KookeeChat.setTheme('dark'); // 'light', 'dark' ou 'auto'
O método é a parte fácil. O que o torna utilizável é o comportamento dele:
Aplica o tema ali mesmo. Sem remontagem: um painel aberto continua aberto e uma resposta em streaming continua chegando. Só as cores mudam.
Funciona antes de o widget carregar. Scripts de widget carregam com
async, e o seu código de tema muitas vezes roda antes. Uma chamada feita cedo deve entrar numa fila, e não ser descartada nem virar erro.A ordem não importa. Se a sua chamada rodou antes ou depois de o widget ler a própria configuração, a chamada explícita vence.
autocontinua sendo uma opção. “Sistema” é uma escolha real no seu seletor, e o widget deve conseguir segui-la ao vivo.
Se você está avaliando um widget de chat ou de suporte e o seu produto tem um seletor de tema, vale conferir esses quatro pontos antes de decidir. O primeiro é onde a maioria das implementações falha sem alarde.
Ligando o widget ao seu seletor de tema
A versão mais simples fica dentro da função que o seu seletor já chama:
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);
}
Repare que a última linha converte system em 'auto', e não no valor que o sistema resolve agora. Passe o valor resolvido e o widget congela naquele momento: quando o sistema do visitante muda para o escuro ao pôr do sol, a sua página acompanha e o widget não. Passe 'auto' e o próprio widget passa a observar o sistema.
Definindo o tema inicial sem piscar
setTheme() cuida das mudanças. Para a primeira renderização, entregue ao widget a escolha que você já conhece, assim que o seu próprio script de tema rodar:
const stored = localStorage.getItem('theme'); // 'light' | 'dark' | 'system' | null
KookeeChat.setTheme(stored === 'light' || stored === 'dark' ? stored : 'auto');
Se isso rodar antes de o script do widget carregar, o pequeno stub com fila do snippet de instalação guarda a chamada até o script chegar — desde que 'setTheme' esteja na lista de métodos do stub. Se você renderiza a página no servidor e já sabe o tema ali, um atributo data-theme na tag script faz o mesmo sem nenhum JavaScript.
Quando o seletor não é seu
Num tema da Shopify, num tema de WordPress ou num design system que cuida do modo escuro por você, pode não existir uma função de seletor onde acrescentar uma linha. Nesse caso, observe o resultado:
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();
Ajuste a verificação ao jeito como o seu tema marca o modo escuro — um atributo data-theme="dark" é tão comum quanto uma classe. Aqui o valor resolvido é passado de propósito: quando você só enxerga o resultado, seguir a página à risca é o certo, e o tema resolve “sistema” por você.
Em React
Quando o widget é um componente React, o tema é só uma prop, e passar a configuração atual é todo o trabalho. Com next-themes:
const { theme } = useTheme(); // 'light' | 'dark' | 'system' | undefined
<KookeeChatWidget
clientOptions={{ apiKey }}
theme={theme === 'light' || theme === 'dark' ? theme : 'auto'}
/>;
Use theme, não resolvedTheme. O resolvedTheme é desconhecido durante a renderização no servidor e na primeira renderização no cliente, então um widget alimentado por ele começa claro e muda um instante depois para todo visitante em modo escuro. O theme convertido desse jeito cai em 'auto', que já é o certo para a maioria deles.
Em resumo
auto segue o sistema operacional, e um seletor no seu site é, por definição, algo que o sistema operacional não conhece. Um widget que fica na sua página deve pegar o tema da sua página — em tempo de execução, sem remontar e sem perder o que o visitante estava fazendo.
O widget de chat com IA do Kookee já faz isso. Pela tag script, chame KookeeChat.setTheme(); em React, a prop theme do KookeeChatWidget atualiza ao vivo. O widget React é a única exceção à regra do shadow root descrita acima: ele renderiza na sua página, e as variáveis --kookee-* que ele lê são documentadas, então é seguro defini-las. Veja Personalizar a aparência do widget de chat para as opções de tema e Controlar o widget de chat por JavaScript para a API completa.