Вы выпустили тёмную тему. Посетитель нажимает на значок луны, вся страница плавно темнеет — а в правом нижнем углу как ни в чём не бывало светится белый прямоугольник. Чат поддержки.
Мелочь, но из тех, что бросаются в глаза: это единственная часть страницы, которую явно делали не вы. Ниже — почему встраиваемые виджеты так часто ошибаются, почему очевидные исправления не работают и что должен уметь виджет, чтобы ваш не ошибался.
Тёмных тем две, а не одна
Под «тёмной темой» на сайте понимают одно из двух.
Настройка операционной системы. Посетитель включил на ноутбуке тёмное оформление. Браузер сообщает об этом через медиазапрос
prefers-color-scheme.Настройка вашего сайта. Переключатель в шапке: выбор хранится в
localStorageили в cookie и обычно применяется как классdarkили атрибутdata-themeна<html>.
Большинство сайтов с переключателем предлагают три варианта — светлая, тёмная и системная — как раз потому, что эти две настройки расходятся. Многие держат светлую ОС и всё равно хотят тёмную документацию, тёмную панель управления, тёмное что угодно по ночам.
Сторонний виджет видит первую настройку. Вторую он не видит, пока вы ему не скажете.
Почему «auto» недостаточно
Почти у каждого встраиваемого виджета есть настройка темы, и почти каждый предлагает auto:
<script src="https://widget.example.com/chat.js" data-theme="auto"></script>
auto значит «следовать prefers-color-scheme». Для сайта без переключателя это ровно то, что нужно, поэтому auto — разумное значение по умолчанию. И ровно то, что не нужно, как только у сайта появляется свой переключатель: посетитель со светлой ОС, который выбрал на сайте тёмную тему, получает тёмную страницу и светлый виджет. Медиазапрос не видит класс на вашем элементе <html>, потому что на вашу страницу он вообще не смотрит.
Если жёстко прописать data-theme="dark", проблема просто переедет к посетителям, которые выбрали светлую тему.
Почему это не исправить через CSS
Следующее желание — залезть внутрь и перекрасить виджет:
.dark .chat-panel { background: #111; color: #eee; }
На хорошо сделанном виджете это ничего не даст, и так задумано. Виджеты, которые живут на чужих страницах, рендерятся внутри shadow root, чтобы CSS страницы не ломал их, а их CSS не ломал страницу. Ваш селектор эту границу не пересекает.
Некоторые виджеты пропускают внутрь несколько CSS-переменных, и их хочется задать из вашего класса тёмной темы. Не стоит: эти имена — внутреннее устройство виджета, а не его API. Они меняются без записи в журнале изменений, и ваша тёмная тема тихо ломается на релизе, которого вы даже не заметили.
Почему перезагрузка ещё хуже
В документации некоторых виджетов советуют действовать топорно: уничтожить виджет и создать его заново с новой темой.
Widget.destroy();
Widget.init({ ...config, theme: 'dark' });
Это работает в том смысле, что цвета меняются. Но заодно размонтируется всё, чем посетитель был занят. Открытая панель закрывается. Ответ, который ещё приходил потоком, обрывается на середине предложения. А переключатель часто нажимают именно потому, что читают что-то поздно ночью, — худший момент, чтобы выбросить разговор.
Что должен уметь виджет
Тема принадлежит странице, на которой стоит виджет, поэтому страница должна уметь сообщить виджету, что тема сменилась. Для этого хватает одного метода:
KookeeChat.setTheme('dark'); // 'light', 'dark' или 'auto'
Сам метод — простая часть. Пользоваться им можно, только если он ведёт себя так:
Применяется на месте. Без перемонтирования: открытая панель остаётся открытой, а ответ продолжает приходить потоком. Меняются только цвета.
Работает до загрузки виджета. Скрипты виджетов загружаются с
async, и ваш код темы часто выполняется раньше. Ранний вызов должен вставать в очередь, а не теряться и не превращаться в ошибку.Не зависит от порядка. Выполнился ли ваш вызов до или после того, как виджет прочитал собственную конфигурацию, побеждает явный вызов.
autoостаётся вариантом. «Системная» — полноценный пункт вашего переключателя, и виджет должен уметь следовать за ней на лету.
Если вы выбираете чат или виджет поддержки, а в продукте есть переключатель темы, проверьте эти четыре пункта до того, как определитесь. На первом большинство реализаций тихо спотыкается.
Как связать виджет с вашим переключателем
Проще всего добавить вызов в функцию, которую ваш переключатель уже вызывает:
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);
}
Последняя строка превращает system в 'auto', а не в ту тему, которой system соответствует прямо сейчас. Передайте вычисленное значение, и виджет застынет в этом моменте: когда ОС посетителя на закате переключится на тёмную тему, страница последует за ней, а виджет нет. Передайте 'auto', и виджет будет следить за ОС сам.
Начальная тема без мигания
setTheme() отвечает за смену темы. Для первой отрисовки передайте виджету уже известный выбор — так же рано, как выполняется ваш собственный скрипт темы:
const stored = localStorage.getItem('theme'); // 'light' | 'dark' | 'system' | null
KookeeChat.setTheme(stored === 'light' || stored === 'dark' ? stored : 'auto');
Если этот код выполнится до загрузки скрипта виджета, маленькая заглушка с очередью из кода установки придержит вызов, пока скрипт не загрузится, — при условии, что 'setTheme' есть в её списке методов. Если вы рендерите страницу на сервере и знаете тему уже там, ту же работу сделает атрибут data-theme на теге script, вообще без JavaScript.
Когда переключатель не ваш
В шаблоне Shopify или WordPress или в дизайн-системе, которая сама управляет тёмной темой, может не быть функции переключателя, куда можно дописать строку. Тогда следите за результатом:
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();
Подстройте проверку под то, как ваш шаблон помечает тёмную тему: атрибут data-theme="dark" встречается так же часто, как класс. Здесь вычисленное значение передаётся намеренно: когда вы видите только результат, правильно точно следовать за страницей, а «системный» вариант за вас уже разрешил шаблон.
В React
Когда виджет — React-компонент, тема — просто проп, и вся работа сводится к тому, чтобы передать текущую настройку. С next-themes:
const { theme } = useTheme(); // 'light' | 'dark' | 'system' | undefined
<KookeeChatWidget
clientOptions={{ apiKey }}
theme={theme === 'light' || theme === 'dark' ? theme : 'auto'}
/>;
Берите theme, а не resolvedTheme. resolvedTheme неизвестен при серверном рендеринге и при первом рендере на клиенте, поэтому виджет, который получает его, у каждого посетителя с тёмной темой сначала светлый, а через мгновение перекрашивается. theme, преобразованный таким образом, по умолчанию даёт 'auto', и для большинства таких посетителей это уже правильно.
Тему виджету задаёт страница
auto следует за операционной системой, а о переключателе на вашем сайте операционная система по определению ничего не знает. Виджет, который стоит на вашей странице, должен брать тему с вашей страницы — во время работы, на месте и не теряя того, чем был занят посетитель.
Виджет Kookee для сайта уже это умеет. При подключении через тег script вызывайте KookeeChat.setTheme(), в React проп theme у KookeeChatWidget обновляется на лету. React-виджет — единственное исключение из правила про shadow root выше: он рендерится прямо в вашей странице, а переменные --kookee-*, которые он читает, описаны в документации, так что задавать их безопасно. Настройки темы описаны в статье Внешний вид чат-виджета, а весь API — в статье Управление чат-виджетом из JavaScript.