Перемикач тем, який вас не підведе: кнопка, випадач і збережена перевага
Хтось неодмінно поскаржиться на ваш перемикач тем. Якщо ви випустите лише світлий режим, вам напишуть о 2-й ночі з телефону в темній кімнаті. Якщо випустите темний режим із одним кадром білого миготіння, поскаржаться через головний біль. Якщо перевага не зберігається — скарги будуть при кожному завантаженні сторінки; це особливий вид скарги, що змушує інженерів сумніватися в кар’єрі.
Це практичний посібник для продакшену з побудови інтерфейсу перемиканння тем, використовуючи простий HTML/CSS і трохи JavaScript: кнопка-перемикач, випадач з опцією «Система» та збережена перевага. Мета — не «працює на моєму комп’ютері», а «працює в реальному світі, де браузери блокують сховище, сторінки кешуються, а хтось вимірює ваш CLS».
Вимоги, які важливі в продакшені
Перемикання тем виглядає як шліфування інтерфейсу, поки ви не розгорнете його в масштабі. Тоді воно перетворюється на фічу надійності: зачіпає продуктивність, доступність, кешування і часом перевірки безпеки (усе, що пише в сховище, викликає питання). Визначте вимоги заздалегідь, бо «темний режим» — це не вимога; це купа прикордонних випадків у тренчкотові.
Базові вимоги (без компромісів)
- Логіка з трьома станами:
light,darkіsystem. Користувачі очікують «слідувати системі». Підприємства також це очікують, бо IT задає політики системи. - Персистентність: запам’ятовувати явний вибір користувача між перезавантаженнями і сесіями. Якщо сховище заблоковане — деградувати плавно.
- Без миготіння неправильної теми: уникати «білої спалахи» під час першого рендеру, коли користувач віддає перевагу темній темі. Це не косметика; це пошкодження UX.
- Доступні керування: кнопка-перемикач з коректними підписами і випадач, що підтримує клавіатуру. Якщо це недоступно — це не завершено.
- Низька складність: без зайвих фреймворків. Малий JS. Прості CSS. Менше площі атаки — менше багів.
- Безпечні значення за замовчуванням: якщо щось падає (винятки в сховищі, JS вимкнений), сторінка лишається читабельною.
Побажання, що окупаються
- Кілька тем: навіть якщо сьогодні лише світла/темна, структуруйте CSS так, щоб додати «сепія» або «високий контраст» не означало перепис всього.
- Телеметрія: не треба нав’язливої аналітики, але потрібен спосіб дізнатися, чи провалюється збереження переваги у великої когорти.
- Сумісність компонентів: якщо ваш додаток вставляє сторонні віджети, вирішіть, чи вони наслідують тему, чи залишаються фіксованими.
Цитата, яку варто приклеїти до монітора: «Сподівання — не стратегія.» (парафраз ідеї, приписують генералу Гордону Р. Саллівану) Перемикання тем потребує того ж підходу: визначте режими відмови, а потім проектуйте навколо них.
Жарт №1: Якщо ваш перемикач тем викликає ефект світлошумової гранати опівночі — вітаємо, ви винайшли новий тип оповіщення on-call.
Проєкт контракту: теми, джерела та пріоритети
Більшість перемикачів тем ламаються, бо ніхто не записав контракт. Потрібне просте, явне дерево рішень, яке можна імплементувати раз і забути. UI має бути уявленням цього контракту, а не самим контрактом.
Модель теми
Визначте дві окремі концепції:
- Перевага: що вибрав користувач:
system,light,dark,sepia… - Ефективна тема: що застосовано зараз: зазвичай
light,darkабоsepia.
«Система» — це перевага, а не тема. Коли перевага — system, ви обчислюєте ефективну тему за допомогою prefers-color-scheme. Якщо їх змішати, ваш JS почне перезаписувати перевагу користувача при зміні ОС — і користувачі розлютяться, бо вони нічого не вибирали.
Порядок пріоритетів (не імпровізуйте)
- Явна перевага користувача, збережена клієнтськи (localStorage, cookie, профіль на сервері) — перемагає.
- Системна перевага через
prefers-color-scheme, якщо перевага користувача —systemабо не встановлена. - Значення за замовчуванням (зазвичай світла), якщо виявлення не вдалось або JS вимкнений.
Стан, корисний для налагодження
Мені подобається зберігати два атрибути dataset на <html>:
data-theme=light/dark/sepia(ефективна)data-theme-source=explicit/system/fallback
Це здається тривіальним, доки ви не будете налагоджувати скаргу «тема випадково скидається» у браузері з підвищеною приватністю. Джерело підкаже, чи спрацювало сховище.
HTML UI: кнопка + випадач без боргу по доступності
UI має дві задачі: (1) дозволити користувачам перемикати, (2) повідомляти про поточний стан. Кнопка одна прекрасно підходить для двох тем, але ламається, коли додаєте «Система» або «Сепія». Випадач зрозуміліший і масштабований. Наявність обох здається зайвою — доки ви не намагаєтесь зробити щось, що працює для користувачів з клавіатурою, просунутих користувачів і людей, яким просто потрібно, щоб сторінка не засліплювала їх.
Що ми випускаємо
- Кнопка-перемикач, що міняє між
lightіdark. Вона не встановлює «system». Це швидка дія. - Випадач тем, що пропонує
system,light,darkіsepia.
Вибори щодо доступності
- Використовуйте реальні
<button>і<select>. Нативні контролі дають багато доступності «безкоштовно». - Підписуйте контролі через
aria-labelабо видимі підписи. Уникайте кнопок тільки з іконкою без підпису, якщо вам подобаються злі звіти аудиту. - Не перенавантажуйте ARIA для простих віджетів. ARIA — не набір інструментів do-it-yourself; це гострий інструмент.
HTML у шапці цієї сторінки — референтна реалізація. Копіюйте її. Змінюйте id, якщо потрібно. Зберігайте семантику.
CSS-стратегія: змінні, color-scheme і розумні значення за замовчуванням
Найпростіший спосіб зробити темінг витривалим — використовувати CSS-змінні для всіх кольорових токенів і встановити їх на :root та [data-theme="…"]. Якщо ваш CSS повний жорстко прописаних hex-значень по всіх компонентах, у вас не теми. У вас майбутній інцидент.
Використовуйте токени, а не атмосфери
Почніть з невеликого набору токенів:
--bg,--fg--mutedдля вторинного тексту--cardдля поверхонь--border--link--focusдля фокусних кілець
Це мінімальний життєздатний набір токенів, який позбавить вас необхідності перетворювати кожен компонент вручну.
color-scheme — не опціонально
Сучасні браузери використовують color-scheme для вирішення, як відмалювати вбудований UI (скролбари, елементи форм в окремих контекстах) і для оптимізації рендерингу. Якщо ви встановите темні кольори, але забудете color-scheme: dark;, ви отримаєте «темну сторінку, яскраві інпути» або іншу несумісність UI.
У CSS вище кожна тема встановлює color-scheme. Це навмисно.
Надавайте перевагу селекторам атрибутів на <html>
Поставте data-theme на <html> (documentElement). Це охоплює тему на весь документ і добре працює з вбудованим контентом. Уникайте встановлення класів теми на <body>, якщо у вас є скрипти, що замінюють вміст body, або якщо ви робите рендеринг на сервері з частковими шаблонами; це створить дивні переходи під час гідратації.
Переходи: використовуйте їх обережно
Людям подобаються плавні фейди. SRE люблять передбачувану поведінку. Якщо ви додасте глобальні transition типу transition: background-color 250ms; на *, рано чи пізно ви щось зламаєте (наприклад, скелетони або графіки). Якщо ви додаєте переходи взагалі, обмежте їх кількама елементами і поважайте reduced motion:
- Використовуйте
@media (prefers-reduced-motion: reduce), щоб відключити переходи. - Не робіть transition для
color-scheme. Це не його вечірка.
Маленький JS, який справді надійний (і уникає миготіння)
Існує два моменти JavaScript, які важливі:
- Ранній старт: оберіть ефективну тему до першого рендеру, щоб уникнути миготіння.
- Взаємодія: оновіть тему при взаємодії користувача та збережіть її.
Розміщення скрипта для раннього старту
Помістіть невеликий скрипт у <head>, який виконається до застосування CSS. Він має:
- Спробувати прочитати збережену перевагу з
localStorage. - Якщо збережена перевага —
systemабо відсутня, визначити її черезprefers-color-scheme. - Відразу встановити
document.documentElement.dataset.theme. - Ловити винятки зі сховища (так, таке буває) і безпечно відкотитися.
Хед-скрипт у цьому документі робить саме це. Він використовує try/catch, бо деякі браузери кидають помилки при доступі до сховища в певних режимах приватності, і бо ви можете вбудовувати цю сторінку в контекст, що забороняє збереження.
Скрипт взаємодії (те, що багато хто забуває загартувати)
Нижче — решта JS. Він синхронізує випадач, робить кнопку-перемикач працездатною, слухає зміни системної теми коли перевага — system, і зберігає перевагу.
cr0x@server:~$ cat theme-switcher.js
(function(){
var storageKey = "theme.preference";
var root = document.documentElement;
var btn = document.getElementById("theme-toggle");
var sel = document.getElementById("theme-select");
if (!btn || !sel) return;
function getSystemTheme() {
var mql = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
return (mql && mql.matches) ? "dark" : "light";
}
function getSavedPreference() {
try { return localStorage.getItem(storageKey); }
catch (e) { return null; }
}
function savePreference(pref) {
try { localStorage.setItem(storageKey, pref); }
catch (e) { /* ignore */ }
}
function applyTheme(pref) {
var effective = pref;
if (!effective || effective === "system") {
effective = getSystemTheme();
root.dataset.themeSource = "system";
} else {
root.dataset.themeSource = "explicit";
}
root.dataset.theme = effective;
sel.value = pref || "system";
btn.setAttribute("aria-label", "Toggle theme (currently " + effective + ")");
}
function toggleLightDark() {
var current = root.dataset.theme || "light";
var next = (current === "dark") ? "light" : "dark";
savePreference(next);
applyTheme(next);
}
// Initialize UI from saved preference (or system).
var pref = getSavedPreference() || "system";
applyTheme(pref);
btn.addEventListener("click", function(){
toggleLightDark();
});
sel.addEventListener("change", function(){
var pref = sel.value;
savePreference(pref);
applyTheme(pref);
});
// If user follows system, respond to system changes.
var mql = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
if (mql && mql.addEventListener) {
mql.addEventListener("change", function(){
var pref = getSavedPreference() || sel.value || "system";
if (pref === "system") applyTheme("system");
});
} else if (mql && mql.addListener) {
// Older Safari
mql.addListener(function(){
var pref = getSavedPreference() || sel.value || "system";
if (pref === "system") applyTheme("system");
});
}
})();
Зауваження, що мають значення:
- Ми зберігаємо перевагу, а не ефективну тему. Так «system» залишається осмисленим.
- Ми оновлюємо UI після застосування теми. Це уникає невідповідності, коли випадач показує «System», а ви примусово нав’язуєте темну тему.
- Ми слухаємо зміни ОС лише коли перевага — system. Інакше ви перезаписуєте явний вибір користувача, що швидко призведе до багрепорту від важливої людини.
Жарт №2: Перемикач тем без персистентності — як кавоварка, що щоранку забуває «міцно» — технічно працює, але емоційно неприйнятна.
Факти та історичний контекст (так, це важливо)
Перемикання тем — не новинка, але платформа браузерів навколо нього різко змінилась. Декілька корисних фактів допоможуть приймати кращі рішення і уникати cargo-cult коду.
- Раніше «теми» часто були основані на зображеннях. У 2000-х «скинінг» часто означав заміну фонів і спрайтів. Виглядало круто, але завантажувалося як вантажівка цегли.
- Системні теми зробили перевагу переносимою. Коли ОС додали системні налаштування зовнішнього вигляду, користувачі перестали думати «цей додаток має темну тему» і почали очікувати, що все слідуватиме їхньому пристрою.
prefers-color-schemeзмінив очікування за замовчуванням. Коли браузери відкрили системну перевагу через медіа-запити, ігнорувати її стало питанням доступності і комфорту, а не стилю.- «Миготіння неакутованого контенту» існувало до темного режиму. FOUC спочатку був про пізнє завантаження CSS і показ неоформленого HTML. Миготіння теми — сучасна варіація того самого.
color-schemeвідносно новий і його легко пропустити. Він появився тому, що елементи форм і скролбари не завжди відмалюються лише CSS. Без нього ви отримаєте непослідовні елементи UI.- localStorage синхронний. Через це він зручний для раннього старту, але небезпечний для важких записів. Зчитування зазвичай ок; великі записи на гарячих шляху — ні.
- Деякі середовища кидають помилки при доступі до сховища. Режими приватного перегляду, вбудовані вебвю і строгі налаштування приватності можуть призвести до того, що
localStorageкине виняток замість повернення null. - Корпоративний IT може примусово встановлювати політики зовнішнього вигляду. Опція «system» уніфікує ваш додаток з керованими десктопами. Без неї ви воюватимете з політикою CSS.
Три корпоративні історії з фронту тем
Інцидент: неправильне припущення, що «system» — це тема
Середнього розміру внутрішня панель випустила випадач з трьома опціями: Light, Dark і System. Під капотом зберігалося те, що показував випадач, у localStorage і застосовувалося як клас на <body>. CSS мав правила для .light і .dark. Розв’язку сюжету ви вже бачите.
На перший день все виглядало нормально, бо більшість вибирали Light або Dark. Але опція «System» була популярна серед ноутбучних користувачів, що переходили між офісом і домом. Коли вони обирали System, додаток зберігав system і застосовував клас system. CSS для нього не було, тож сторінка впала на стилі за замовчуванням — здебільшого світла тема, крім компонентів, частково перероблених під змінні. Результат: змішана тема. Невідповідності контрасту. Кнопки, що виглядали неактивними, хоча не були.
Квитки підтримки почали надходити як «випадково» зіпсований UI. Інженери спочатку підозрювали кешування або часткові деплоя через неповторність. Але це відтворювалось послідовно; треба було тільки вибрати «System». Помилка була в припущенні: трактувати «system» як тему, а не як перевагу, яку треба розв’язати в ефективну тему.
Виправлення було нудним і швидким: зберігати перевагу окремо, обчислювати ефективну тему під час виконання і встановлювати один атрибут на <html>. Також додали data-theme-source для перевірок. Наступного разу, коли хтось кричав «це випадково», вже було простіше діагностувати.
Оптимізація, що зіграла злий жарт: надто агресивне кешування теми
Команда електронної комерції захотіла усунути миготіння теми і зробила рендер теми на сервері з cookie. Ідея хороша. Додали реверс-проксі кеш і почали агресивно кешувати HTML для незалогінених користувачів. Все добре, якщо бути уважним.
Вони не були уважні. Кешували HTML-відповіді без варіювання за cookie. Це означало, що перший запит після пропаду кешу заповнив кеш світлою або темною версією залежно від того, хто зайшов першим. Усі інші отримували цей кешований варіант. Користувачі бачили «випадкову» зміну тем між відвідинами, бо кеш обертався за терміном, а не за перевагою.
Гірше: CSS-файл містив вбудовані змінні теми, згенеровані на сервері. Тож отруєння кешу торкнулося не лише HTML, а й CSS. Команда звинувачувала браузери, потім CDN, потім повний місяць. Тим часом користувачі бачили непослідовність і вважали сайт глючним.
Відкат був болючим, бо зміна кешування зачепила бюджети продуктивності. Остаточне виправлення: варіювати кеш за cookie коли вона присутня, але також зберігати надійний клієнтський резольвер як підстраховку. І перестали генерувати theme-specific CSS на запит; відправили статичний CSS з data-атрибутами. «Оптимізація» тимчасово стала податком на стабільність, поки не змінили архітектуру.
Нудно, але правильно: маленький head-скрипт, що врятував ситуацію
Фінансова команда створила внутрішній звітний інструмент, що використовується на великих моніторах у світлому офісі і на ноутбуках у затемнених конференц-залах. Вони були суворі щодо доступності через аудити. UI був серверно відрендерений з невеликим JS.
Коли додали темний режим, перший прототип був «ок», але мав звичне миготіння при завантаженні. У девелопі це не так видно, бо локальні машини швидкі. У продакшені з реальною латентністю і вбудованим скриптом аналітики миготіння було дуже помітне.
Замість перепису додатка або підключення бібліотек тем вони зробили неспекотну річ: додали 20-рядковий head-скрипт, що читає перевагу і встановлює data-theme до завантаження CSS. Також написали один інтеграційний тест, що завантажує сторінку з попередньо встановленим localStorage і перевіряє, що перший рендер вже тематизований.
Ось і все. Ніяких героїчних вчинків. Ніякого переписування платформи. Аудитори перестали знаходити проблеми контрасту, on-call — отримувати скарги «мені болять очі». Це нагадування: «нудне» рішення часто є надійним.
Практичні завдання: команди, очікуваний результат і рішення
Ви можете зібрати це в codepen і закінчити день. Або ж розгорнути в реальному середовищі, де існують кроки збірки, кешування, заголовки та регресії. Ось операційні завдання, які я справді зробив би (або попросив би зробити), перш ніж вважати фічу готовою до продакшену.
Завдання 1: Перевірте, що ваш HTML встановлює data-theme до першого рендеру (прості grep)
cr0x@server:~$ grep -n "document.documentElement.dataset.theme" -n index.html
42: document.documentElement.dataset.theme = theme;
48: document.documentElement.dataset.theme = "light";
Що це означає: У вас є ранній сеттер. Якщо він є лише в відкладеному бандлі, ви все одно будете мигати.
Рішення: Якщо сеттер не в head або виконується занадто пізно, перемістіть його в inline-скрипт у head.
Завдання 2: Підтвердіть, що скрипт в head виконується перед зовнішнім CSS (перевірка порядку)
cr0x@server:~$ awk 'NR<=80{print NR ":" $0}' index.html
1:
2:
3:
4:
5:
6: Theme Switcher UI That Doesn’t Betray You: Button, Dropdown, and Remembered Preference
...
23:
113:
114:
Що це означає: У цьому прикладі CSS вбудований, а boot-скрипт після нього, що все ще ок, бо скрипт виконується під час парсингу, але треба бути свідомим. Для зовнішнього CSS бажано мати boot-скрипт перед завантаженням CSS або принаймні перед рендером.
Рішення: Якщо ви використовуєте зовнішні CSS-файли, помістіть boot-скрипт вище <link rel="stylesheet"> або вбудуйте мінімальний CSS і встановіть тему до повного застосування стилів.
Завдання 3: Перевірте винятки сховища в жорсткому середовищі
cr0x@server:~$ node -e 'console.log("Simulate: localStorage may throw in some browsers; ensure try/catch exists")'
Simulate: localStorage may throw in some browsers; ensure try/catch exists
Що це означає: Це завдання про процес: треба кодувати так, ніби сховище може не працювати. Ви не зможете надійно відтворити всі режими приватності в CI.
Рішення: Тримайте try/catch навколо читання/запису. Вважаємо відсутність переваги як «system».
Завдання 4: Переконайтесь, що бандл JS не перезаписує ранню тему
cr0x@server:~$ grep -R --line-number "dataset.theme =" dist/ | head
dist/app.js:812:root.dataset.theme = effective;
Що це означає: Ваш основний JS теж встановлює тему, що ок, якщо він використовує ту саму логіку і ключ переваги. Не ок, якщо він завжди за замовчуванням ставить світлу.
Рішення: Переконайтесь, що і ранній boot, і код взаємодії використовують однакове джерело переваги і правила пріоритету.
Завдання 5: Перевірте, що в CSS немає жорстко прописаних кольорів, які ламають теми
cr0x@server:~$ grep -R --line-number -E "#[0-9a-fA-F]{3,6}\b|rgb\(|hsl\(" src/styles | head
src/styles/components/buttons.css:12: border: 1px solid var(--border);
src/styles/components/layout.css:4: background: var(--bg);
Що це означає: Ідеально, якщо grep переважно знаходить використання токенів. Якщо знайдено випадкові hex-коди, вони, ймовірно, виглядатимуть неправильно в одній з тем.
Рішення: Замініть кольори компонентів на змінні. Залиште кілька «брендових» акцентів тільки якщо ви протестували їх в обох режимах.
Завдання 6: Запустіть lint на випадкові глобальні переходи
cr0x@server:~$ grep -R --line-number "transition:" src/styles | head
src/styles/base.css:88: transition: background-color 250ms ease, color 250ms ease;
Що це означає: Глобальні переходи можуть додати лаги і неочікувані анімації в графіках або скелетонах.
Рішення: Якщо переходи застосовані до великих DOM-піддерев, обмежте їх невеликою кількістю контейнерів або видаліть.
Завдання 7: Перевірте, чи сервований HTML не кешується неправильно при використанні cookie
cr0x@server:~$ curl -I -H "Cookie: theme=dark" http://localhost:8080/ | egrep -i "cache-control|vary|set-cookie"
Cache-Control: public, max-age=300
Vary: Accept-Encoding
Що це означає: Якщо ви подаєте тему через cookie і кешуєте HTML, вам, ймовірно, потрібен Vary: Cookie або відключення кешування для персоналізованого HTML. Вивід вище не варіює за cookie, тож кеш може змішувати теми.
Рішення: Або уникайте серверного темінгу для кешованих сторінок, або сегментуйте кеш правильно.
Завдання 8: Підтвердіть, що ваш CSP дозволяє inline head-скрипт (або заплануйте nonce)
cr0x@server:~$ curl -I http://localhost:8080/ | egrep -i "content-security-policy"
Content-Security-Policy: default-src 'self'; script-src 'self'
Що це означає: script-src 'self' блокує inline-скрипти. Ваш ранній inline-скрипт не виконається.
Рішення: Додайте nonce для inline-скрипту або завантажте маленький зовнішній boot-скрипт з високим пріоритетом. Якщо не можете — прийміть можливість миготіння.
Завдання 9: Перевірте поведінку prefers-color-scheme у headless smoke-тестах
cr0x@server:~$ node -e 'console.log("Headless browsers may default to light; set emulation if you rely on prefers-color-scheme in tests.")'
Headless browsers may default to light; set emulation if you rely on prefers-color-scheme in tests.
Що це означає: У CI headless-браузери можуть за замовчуванням бути світлими. Тести, що припускають темну за замовчуванням, будуть падати непередбачувано.
Рішення: У E2E-тестах явно встановлюйте перевагу (через сховище або емулювання) і асертуйте data-theme.
Завдання 10: Виміряйте, чи міг би бути помітний flash теми (швидкий sniff продуктивності)
cr0x@server:~$ google-chrome --headless --disable-gpu --dump-dom http://localhost:8080/ | head
<!doctype html><html lang="en" data-theme="light" data-theme-source="system">...
Що це означає: Dumped DOM показує, що атрибут теми встановлений рано. Це не повністю доводить відсутність миготіння (час малювання важко відтворити в headless), але дає корисну перевірку.
Рішення: Якщо data-theme відсутній у виведеному DOM, ваш ранній boot не виконався, ймовірно через CSP або порядок завантаження.
Завдання 11: Підтвердіть, що елементи керування UI відповідають збереженій перевазі
cr0x@server:~$ node -e 'console.log("Manual check: select value should show system/light/dark/sepia, not always default. Verify after reload.")'
Manual check: select value should show system/light/dark/sepia, not always default. Verify after reload.
Що це означає: Якщо випадач завжди скидається візуально, користувачі подумають, що налаштування не збереглось, навіть якщо це не так.
Рішення: Переконайтесь, що ви встановлюєте select.value збереженою перевагою, а не з ефективної теми.
Завдання 12: Перевірте, чи HTML не обрізається санітайзером, що видаляє атрибути теми
cr0x@server:~$ curl -s http://localhost:8080/ | head -n 3
Що це означає: Деякі шаблонізатори або санітайзери видаляють незнайомі атрибути. Якщо data-theme зникає, CSS не застосує тему як очікується.
Рішення: Якщо атрибути обрізаються, застосуйте тему через клас або налаштуйте санітайзер/шаблонізатор, щоб дозволити data-*.
Завдання 13: Перевірте, що сторонні віджети не хардкодять кольори
cr0x@server:~$ grep -R --line-number "style=" public/widgets | head
public/widgets/legacy-chat.html:17:<div style="background:#fff;color:#000">Chat</div>
Що це означає: Inline-стилі ігноруватимуть вашу систему токенів. У темному режимі ви отримаєте яскраві блоки всередині темної сторінки.
Рішення: Рефакторьте стилі віджета під змінні або візуально ізолюйте його (оформіть як «завжди світлий»), щоб виглядало свідомо.
Завдання 14: Переконайтесь, що сервер не компресує/не змінює inline-скрипти так, що вони ламаються
cr0x@server:~$ curl -s -D - http://localhost:8080/ -o /dev/null | egrep -i "content-encoding|content-type"
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Що це означає: Стиснення ок. Вас цікавить несподіване перетворення (деякі «оптимізатори» переписують inline-скрипти).
Рішення: Якщо ви використовуєте middleware для переписування HTML, виключіть head-boot-скрипт з трансформацій або перемістіть його у статичний файл.
Швидкий план діагностики
Коли перемикання тем ламається, люди описують симптоми як «випадково», «миготить» або «ігнорує мій вибір». Не ганяйтесь за відчуттями. Виконайте точну триаж-послідовність, що швидко виведе вас до корінної причини.
Спочатку: визначте, чи проблема в персистентності, розв’язанні чи часі відмалювання
- Перевірка персистентності: встановіть Dark, перезавантажте. Чи лишається темна? Якщо ні — сховище не читається або не записується.
- Перевірка розв’язання: якщо перевага — «System», чи змінюється тема при зміні ОС? Якщо ні — відсутній слухач медіа-запитів або він зламаний.
- Перевірка часу відмалювання: чи правильна тема застосовується врешті, але спочатку ви бачите миготіння? Тоді boot-скрипт занадто пізно або заблокований CSP.
Друге: інспектуйте фактичне джерело істини в DOM
- Подивіться на
<html data-theme="..." data-theme-source="...">. Якщоdata-theme-source=fallback, схоже, що сховище кидало виняток. - Підтвердіть, що значення випадача відображає перевагу, а не ефективну тему.
Третє: перевірте кеш і політику
- Якщо ви використовуєте cookie для теми на сервері, перевірте заголовки кешу і
Vary. - Перевірте CSP. Inline-boot-скрипти часто гинуть тут.
- Перевірте, чи політика корпоративного браузера не вимикає постійне сховище для сайту.
Боттлнек-питання
Звичайний вузький елемент — не CPU. Це порядок: браузер малює до того, як виконано ваше рішення про тему. Виправте це першим. Потім думайте про елегантність.
Поширені помилки: симптом → корінь → виправлення
1) Симптом: «Темний режим миготить білим при перезавантаженні»
Корінь: Тема застосована після першого рендеру (JS-бандл відкладений або виконується після завантаження CSS).
Виправлення: Вбудуйте мінімальний head-boot-скрипт, щоб встановити data-theme до рендеру; забезпечте, що CSP дозволяє або використайте nonce/статичний boot-файл.
2) Симптом: «Я вибираю System і інтерфейс напівтематизований»
Корінь: Трактування system як клас теми замість розв’язання його в ефективну тему.
Виправлення: Зберігайте перевагу (system), але застосовуйте ефективну тему (light/dark) до data-theme. Не створюйте .system CSS, якщо ви цього не маєте на увазі.
3) Симптом: «Налаштування не зберігається, працює тільки до закриття вкладки»
Корінь: Невірно використовується sessionStorage, або записи в сховище падають через винятки.
Виправлення: Використовуйте localStorage з try/catch, або відкотіться до cookie. Зробіть додаток робочим, якщо персистентність недоступна.
4) Симптом: «Випадач каже System, але сторінка примусово в Dark»
Корінь: Стан UI виводиться з ефективної теми замість збереженої переваги.
Виправлення: Завжди встановлюйте значення випадача з переваги; обчислюйте ефективну тему окремо.
5) Симптом: «Тема змінюється сама по собі при зміні ОС, хоча я вибрав Dark»
Корінь: Слухаєте prefers-color-scheme зміни без умов і перезаписуєте поведінку.
Виправлення: Реагуйте на системні зміни лише коли перевага — system.
6) Симптом: «Інпути і скролбари лишаються світлими в темній темі»
Корінь: Відсутня декларація color-scheme для темної теми.
Виправлення: Встановіть color-scheme: dark; всередині селектора вашої теми.
7) Симптом: «Користувачі повідомляють про випадкову тему, але тільки в продакшені»
Корінь: Кеш змішує контент між варіантами тем (темінг на основі cookie + спільний кеш), або CSP блокує ранній boot-скрипт.
Виправлення: Виправте кешування (vary/disable для персоналізованого HTML) і підтвердіть, що CSP підтримує bootstrap теми.
8) Симптом: «Перемикач тем працює, але сторінка стає повільною»
Корінь: Великі DOM-оновлення через переходи або перерахунки; іноді через застосування теми до багатьох вузлів по черзі.
Виправлення: Застосовуйте тему на корені (<html>) тільки. Уникайте глобальних переходів. Тримайте токени маленькими.
Чеклісти / покроковий план
Покроково: випустіть надійний перемикач тем
- Визначте переваги: вирішіть допустимі значення переваги (
system,light,dark, опційні додатки). - Визначте токени: оберіть невеликий набір CSS-змінних, які використовують усі компоненти.
- Реалізуйте селектори тем:
:rootдля значень за замовчуванням,[data-theme="dark"]тощо. Тримайте на рівні<html>. - Додайте
color-scheme: світла тема встановлюєlight, темна —dark. - Напишіть head-boot-скрипт: читає перевагу (try/catch), розв’язує системну перевагу, встановлює
data-theme. - Додайте контролі UI: нативна кнопка і select, з підписами, дружні до клавіатури.
- Напишіть скрипт взаємодії: змінює перевагу, зберігає, застосовує ефективну тему, синхронізує UI.
- Обробляйте системні зміни: слухайте
prefers-color-schemeзміни тільки коли перевага —system. - Тестуйте з заблокованим сховищем: підтвердіть, що сторінка залишається читабельною і не кидає помилки, що ламає інші скрипти.
- Тестуйте з CSP: переконайтесь, що inline-boot-скрипт дозволений (nonce) або перемістіть його у статичний файл.
- Тестуйте поведінку кешу: якщо темінг виконується на сервері, переконайтесь у сегментації кешу за перевагою.
- Випустіть і моніторьте: додайте легку логіку для помилок збереження теми, якщо ваше середовище це дозволяє (рахуйте винятки без збору персональних даних).
Пре-флайт чекліст (що я зробив би перед увімкненням за замовчуванням)
- Сторінка завантажується з правильною темою при встановленій перевазі (без видимого миготіння у реальному браузері, а не лише headless).
- Стан випадача і кнопки завжди відображає реальну перевагу/ефективну тему.
- З JS вимкненим сторінка лишається читабельною і контролі не вводять в оману.
- При заблокованому сховищі вибір теми працює для сесії (навіть якщо не зберігається) і не ламає сторінку.
- Перевірки контрасту проходять для тексту, вторинного тексту, кнопок і фокусних контурів у всіх темах.
- Немає глобальних переходів, що викликають дивні анімації.
- CSP сумісний з підходом boot-скрипту.
FAQ
1) Чи зберігати ефективну тему або перевагу?
Зберігайте перевагу (system/light/dark). Обчислюйте ефективну тему під час виконання. Інакше «System» втрачає сенс і ви ризикуєте перезаписувати користувача несподівано.
2) Чому не покластися лише на prefers-color-scheme і не робити UI?
Бо користувачі хочуть контролю. Також корпоративні десктопи можуть мати системні налаштування, що не відповідають особистим уподобанням у певних додатках. Дайте їм перевизначення.
3) Чи безпечний localStorage для цього?
Для невеликого рядкового значення — так. Справжня проблема в тому, що він може кидати помилки або бути недоступним у деяких режимах приватності. Загорніть доступ у try/catch і деградуйте плавно.
4) Чому поміщати boot-скрипт inline в head?
Щоб уникнути миготіння неправильної теми. Зовнішні скрипти завантажуються пізніше і можуть бути заблоковані або затримані. Inline-код у head виконується під час парсингу і може встановити data-theme до paint.
5) Мій CSP блокує inline-скрипти. Що робити?
Використайте nonce для inline-скрипту або подайте маленький зовнішній «theme boot» скрипт з високим пріоритетом. Якщо ні — прийміть часткове миготіння і зробіть його менш болісним розумними значеннями за замовчуванням.
6) Чи повинна кнопка циклічно переходити System → Light → Dark?
Ні. Збережіть кнопку як швидкий Light/Dark перемикач. Покладіть System (і додаткові теми) в випадач. Користувачі швидко розуміють таке розділення, і це уникне дивних станів.
7) Чи треба слухати системні зміни теми?
Тільки якщо перевага користувача — system. Інакше це нав’язливо. Також не забудьте, що старіші Safari використовують addListener, а не addEventListener для медіа-запитів.
8) Де прикріплювати атрибут теми: <html> чи <body>?
<html>. Це корінь документа і зменшує дивні ефекти під час гідратації або заміни body. Також узгоджується з поведінкою color-scheme.
9) Як уникнути переписування тонни CSS?
Використовуйте CSS-змінні як токени і робіть компоненти залежними лише від токенів. Тоді визначення тем — лише набори токенів. Це єдиний підхід, що залишається підтримуваним після першого спринту.
10) Чи можна додати «високий контраст» як тему?
Так, і варто розглянути, якщо ваш додаток використовується довгий час. Обробляйте її як будь-яку іншу тему: визначте токени, додайте опцію і ретельно протестуйте контраст та індикатори фокусу.
Висновок: наступні кроки, які можна виконати
Перемикач тем — невелика фіча з дивовижно великою зоною ураження. Правильно зроблений — зникає з суспільного поля уваги: користувачі отримують комфорт, доступність покращується, і ваш UI перестає боротися з ОС. Неправильно зроблений — перетвориться на генератор низькопрофільних інцидентів: квитки про миготіння, звіти про «випадкову поведінку» і повільне падіння довіри.
Наступні кроки:
- Реалізуйте CSS на базі токенів (
:root+[data-theme]) і встановітьcolor-schemeдля кожної теми. - Додайте head-boot-скрипт і перевірте, що він запускається під вашим CSP і налаштуванням кешу.
- Підключіть випадач для збереження переваги, а не ефективної теми, і тримайте кнопку як швидкий Light/Dark перемикач.
- Програйтесь по швидкій діагностиці — навмисно — щоб знати, як виглядають режими відмови, перш ніж користувачі їх знайдуть.