Приветствую тебя, мой дорогой друг, сегодня немного поколдуем с JavaScript, CSS и HTML. Ссылка на заголовок
В рамках одного из заказов, а также ввиду того что я оптимизирую вывод Doxygen, понадобился небольшой легковесный скрипт для переключения тем, и, собственно как всегда - встал вопрос о том что именно делать или не делать. Вариантов решения таких задач масса, самый простой - найти в Яндекс, Google или напрямую в GitHub, но, тут есть несколько нюансов:
- Я только начинаю свою официальную карьеру программиста и намерен полностью восстановить свои навыки программирования на всех языках которыми раньше пользовался(не смотря на то что прошло всего пара лет, считаю что такими вещами нужно заниматься регулярно), тем более что JavaScript априори должен быть в арсенали фронтэндера.
- Как правилло, библиотеки с подобными инструментами весят достаточно много, а мне хотелось бы поменьше нагружать свой сервер и браузеры посетителей сайта моего заказчика.
- Если просто вставить скрипт не заглянув ему под капот, то очень легко можно отхватить какой-нибудь эксплоит который вынесет ваш сайт за 5 минут или скрипт с утечкой памяти что может крайне негативно сказаться на пользовательском опыте в результате чего посетитель может и не вернуться.
- Ну и наконец - моя больная тяга делать всё своими руками, времени сегодня у меня вагон и маленькая тележка, так что могу себе позволить потратить 3-4 часа на данную плюшку.
Что нам понадобится Ссылка на заголовок
Понадобится нам совсем не много: 1 div элемент приправленный простейшим текстовым SVG(векторное изображение), прикрученный к сайту небольшим JavaScript-ом и для пущей надёжности приклееный на синюю изоленту - CSS.
ТЗ Ссылка на заголовок
Трансформируемся в Architect_lvl80 и начинаем клацать пальцами по клаве:
API(интерфейс) Ссылка на заголовок
- переключление на следующую тему:
- установить тему напрямую.
UI(гуюзер интерфейс) Ссылка на заголовок
- переключение тем должно выполняться не мешающей или дополняющей дизайн кнопкой(такойже как у внизу справа).
Функцирнал Ссылка на заголовок
- темы должны меняться циклично, после последней - снова первая(
1-2-3-1-2-3); - если пользователь не выбирает тему сам она должна быть установлена системно(тёмная-светлая);
- выбранная тема должна сохраняться;
- переключение должно быть плавным.
У-у-ух! Ничёсе, отошёл на 5 минут ребёнка укачать и 3 часа сладенько проспал…
Так-так-так… На чём же я остановился-то… Тощно-тощно, пора реализовывать!
С чего начать Ссылка на заголовок
Для начала нам понадобится подготовить то над чем мы будем издеваться - CSS. Итак, вспоминаем что это за зверь такой - СЫЫ, берём любую первую попавшуюся страничку(желательно с текстом, заголовками и ссылками) и переходим в режим Mega-pro-designer_lvl100500. Открываем любимую IDE, создаём 3 файла(index.html, main.css, main.js). У меня имеется дефолтная страничка от Hugo, но можно использовать асолютно любую, на всякий случай(чтоб вам долго не искать) вот код(к стати, я реализовал для Doxygen кнопку копирования, так что когда нибудь она и тут появится, сейчас не до её внедрения 😅):
Создание тем оформления Ссылка на заголовок
Открываем файл main.css и решаем задачу №1: обеспечение наличия светлой и тёмной темы, легко, даже не вспотеем. В CSS есть для этого всё необходимое:
- Переменные: очень мощный инструмент который позволяет изменив одну переменную автоматически применить изменения ко всем селекторам использующим её в качестве значения. Синтаксис весьма прост и понятен, начинаем с двух тире и за ними любой буквенный ANSI-символ, нижнее подчёркивание или дефис, обратите внимание что названия чувствительны к регистру(
--This_Is_My_CSS-Varэто не одно и тоже с--this_is_my_css-var). - Определённый метатег который позволяет браузеру самому выбирать как и что ему показывать на основе настроек операционной системы и называется он
@media. - Также есть определённое свойство-фича которое называется
prefers-color-scheme(грубо говоря - тема по умлчанию в ОС), как правило имеет 2 значения(по крайней мере о которых я пока ещё помню) это без квалификатора и с квалификатором: dark.
Итак, в папке с файлом index.html создаём файл main.css и напихиваем в него нужные нам переменные:
/* Объявляем переменные для всего документа (:root - document.documentElement.style) */
:root {
--bg-color: #ddd; /* цвет фона */
--text-color: #222; /* цвет текста */
--accent-color: #007bff; /* цвет подсветки */
--border-color: #aaa; /* цвет рамки */
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* тень от элемента */
--font-family: 'Arial', sans-serif; /* название шрифта */
--text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3); /* тень от текста */
}
/* Тёмная тема через системные настройки ОС */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #222; /* цвет фона */
--text-color: #ddd; /* цвет текста */
--accent-color: #40a9ff; /* цвет подсветки */
--border-color: #444; /* цвет рамки */
--text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3); /* тень от элемента */
--font-family: 'Arial', sans-serif; /* название шрифта */
}
}
Это, скажем так, установки по умолчанию для светлой и тёмной темы, далее как обычно определяем внешний стиль для тем но с использованием этих переменных директивой var:
body {
background-color: var(--bg-color); /* цвет фщна */
color: var(--text-color); /* цвет текста */
transition: background-color 0.3s ease, color 0.3s ease; /* время анимации перехода */
text-shadow: var(--text-shadow); /* тень от текста */
font-family: var(--font-family); /* название шрифта */
max-width: 756px; /* максимальная ширина элемента */
margin: auto; /* отступ */
}
a {
color: var(--text-color); /* цвет текста ссылки */
transition: background-color 0.3s ease, color 0.3s ease; /* анимация изменения */
text-shadow: var(--text-shadow); /* тень от текста */
text-decoration: underline; /* подчёркивание */
font-family: var(--font-family); /* название шрифт */
font-weight: bold; /**/
}
.button {
background-color: var(--accent-color); /* цвет кнопки */
border: 1px solid var(--border-color); /* рамка вокруг кнопки */
padding: 0.5rem 1rem; /* внуренний отсуп */
border-radius: 4px; /* радиус закругления углов */
font-family: var(--font-family); /* название шрифта */
}
Чтож, посмотрим на сколько хорошо сработал режим Mega-pro-designer_lvl100500, открываем в браузере сохранённый файл index.html и радуемся:
- Светлая:

- Тёмная:

Ай да молодец живущий внутри нас дизайнер, постарался на славу, спасибо ему, пускай возьмёт с полки приожок, там их два, пусть берёт срений… 🤭
Полное содержание файла main.css
/* Базовые переменные (светлая тема по умолчанию) */
:root {
--bg-color: #ddd;
--text-color: #222;
--accent-color: #007bff;
--border-color: #aaa;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--font-family: 'Arial', sans-serif;
--text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3);
}
/* Тёмная тема через системные настройки ОС */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #222;
--text-color: #ddd;
--accent-color: #40a9ff;
--border-color: #444;
--text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3);
--font-family: 'Arial', sans-serif;
}
}
.color-sheme-dark {
--bg-color: #222;
--text-color: #ddd;
--accent-color: #40a9ff;
--border-color: #444;
--text-shadow: 0 2px 4px rgba(150, 150, 150, 0.3);
--font-family: 'Arial', sans-serif;
}
.color-sheme-light {
--bg-color: #ddd;
--text-color: #222;
--accent-color: #007bff;
--border-color: #aaa;
--text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--font-family: 'Arial', sans-serif;
}
.color-sheme-terminal {
--bg-color: #222;
--text-color: #9d9;
--accent-color: #40a9ff;
--border-color: #444;
--text-shadow: 0px 0px 10px rgba(255, 255, 0, 0.9);
--font-family: Monospace;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
text-shadow: var(--text-shadow);
font-family: var(--font-family);
}
a {
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
text-shadow: var(--text-shadow);
text-decoration: underline;
font-family: var(--font-family);
}
.button {
background-color: var(--accent-color);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
color: var(--text-color);
border-radius: 4px;
font-family: var(--font-family);
transition: background-color 0.3s ease, color 0.3s ease;
}
#svg-toggle-theme {
position: fixed;
width: 20px;
height: 20px;
border-radius: 10px;
right: 15px;
top: 15px;
filter: drop-shadow(var(--text-shadow));
cursor: move;
}
#svg-toggle-theme.dragging {
opacity: 0.8; /* визуальная обратная связь */
transform: scale(1.1); /* небольшое увеличение при перетаскивании */
}
Программирование поведения(JavaScript) Ссылка на заголовок
С оформлением разобрались, теперь настало время поговорить о самом скрипте.
Для написания не требуется особой подкотовки, хотя основные аспекты стоит изучить, к примеру - тут.
Нам необходимо создать класс Theme, со следующим интерфейсом(публичными методами):
constructor(): конструктор объекта класса который автоматически вызывается при использовании оператораnew;set(theme): устанавливать конкретную тему по её имени'auto','light','dark';toggle(): переключает темы “по кругу”(1-2-3-1-…).
Для корректной и быстрой работы класса нам потребуются некоторые переменные(поля/свойства) которые будут хранить текущую тему, ссылку на элемент к которому она применяется, названия тем, префикс класса, идентификатор в хранилище:
-
Статичные(для всех объектов,
static):#STORAGE: будет хранить в себе ключ хранилища(идентификатор);#NAME: массив названий тем, первыми тремя всегда должна быть:- [0] =>
'auto'; - [1] =>
'light'; - [2] =>
'dark';
- [0] =>
#AUTO: это индекс темы по умолчанию(системной), далее будет объяснено зачем;#CLASS_PREFIX: это префикс(приставка) названия класса темы.
-
Поля объекта свойства отдельного экземпляра:
#current: текущая тема для конкретного элемента;#element: ссылка на элемент к которому применена тема.
Особо наблюдательные уже, наверное, заметили недостающий элемент - изображение кнопки которое сохранено в формате .svg(векторная графика) в шаблоне index.thml, на всякий случай код приведён ниже:
<!-- Элемент хранящий в себе векторное изображение которое будет использоваться как содержимое кнопки переключения тем -->
<div id="svg-toggle-theme">
<svg width="100%" height="100%" viewBox="0 0 135.46666 135.46667" version="1.1" id="button-toggle-theme-svg" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<path id="path1" style="opacity:inherit;fill:currentColor;fill-opacity:1;stroke:#171717;stroke-width:0" d="M 67.777258,12.777515 A 55,55 0 0 0 12.777515,67.777258 55,55 0 0 0 67.777258,122.77752 55,55 0 0 0 122.77752,67.777258 55,55 0 0 0 67.777258,12.777515 Z m -0.128157,4.95577 V 117.73338 A 50,50 0 0 1 17.733285,67.733333 50,50 0 0 1 67.649101,17.733285 Z"/></svg>
</div>
Чего особенного в данном изображении:
- основной цвет настроен как
currentColor(цвет текста); - прозрачность наследуется от роителя(
opacity:inherit); - размер заполняет всё доступное пространство родительского элемента.
С этим всё понятно, вроде, перейдём к самому написаню Ссылка на заголовок
Пререключение на следующую тему Ссылка на заголовок
Итак для корректной обработки переключения тем нам понадобится не строковое название тем а их индекс, хотя можно конечно обойтись и без них, но ввиду того что я закоренелый С-шник немного поизвращаюсь. В чём суть сего извращения: дань парочке мощных операторов в программировании вцелом - взятию остатка от деления(mod(): ‘%’) и тернарному оператору ([условие] ? [если истина] : [если ложь]). Поскольку автоматическая тема не участвует в переключении можем записать всё в одну длинную строчку:
this.#set(this.#current ? ((this.#current % (Theme.#NAME.length - 1)) + 1) : (1 + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 0 : 1)));
Если текущая тема не системна(this.#current не равно (0 или null)) тогда вычисляем следующую тему по схеме:
((this.#current % (Theme.#NAME.length - 1)) + 1)
- вычитаем из размера массива
Theme.#NAMEединицу; - берём остаток от деления
this.#currentна получившееся число; - прибавляем к остаттку единицу
В противном случае нас ждёт ещё один тернарный оператор который определяет текущую системную тему 0(светлая) или 1(темная) и прибавляет к резльтату единицу, т.к. у светлой темы индекс 1, у тёмной - 2:
(1 + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 0 : 1))
Таким образом какая бы системная тема ни была(светлая или тёмная) скрипт всегда переключит на противоположную, это нужн для отзывчивости интерфейса, нажав кнопку пользователь ощутит изменения сразу.
Всю семмантику работы с переключением тем можно упростить, просто нужно немножечко подумать. Я сделал именно так чтобы добавление новых тем было максимально простым:
- добавляем в конец массива
Theme.#NAMEназвание нашей темы; - создаём селектор в main.css и заполняем его.
Установка выбранной темы Ссылка на заголовок
Установкой выбранной темы занимаются два метода Theme.set внешний set(theme) и внутренний #set(theme). Внешний работает со строковым значением, внтренний с индексом темы в массиве Theme.#NAME.
Theme.set(theme): проверяет корректность переданного аргумента, если таковой находится в масиве имён - применяет вызовом Theme.#set(idx).
Theme.#set(theme): удаляет класс текущей темы, после - изменяет текущий идентификатор #current, затем - добавляет класс выбранной темы.
Полное содержание файла main.js
/** Статические поля служат своеобразным конфигом */
class Theme {
static #STORAGE = 'color-sheme'; /// 'color-sheme' - это не ошибка, а умеышленный пропуск 'c', цель - изолировать данные для предотвращения возможных конфликтов с другими скриптами
static #NAME = ['auto', 'light', 'dark', 'terminal']; /// Массив с именами тем
static #AUTO = 0; /// Тема ОС
static #CLASS_PREFIX = 'color-sheme-'; /// Префикс названия класса. 'sheme' - также как и со #STORAGE
static #BUTTON_SVG_ID = 'svg-toggle-theme';
#current = 0;
#element = null;
constructor() {
this.#element = document.documentElement;
this.#load();
}
#load() {
this.#current = parseInt(localStorage.getItem(Theme.#STORAGE)) || Theme.#AUTO;
this.#element.classList.add(`${Theme.#CLASS_PREFIX}${Theme.#NAME[this.#current]}`);
}
/**
* @param {number} theme
*/
#set(theme) {
if (theme != this.#current) {
this.#element.classList.remove(`${Theme.#CLASS_PREFIX}${Theme.#NAME[this.#current]}`);
// Добавляем новый класс
this.#current = theme;
this.#element.classList.add(`${Theme.#CLASS_PREFIX}${Theme.#NAME[this.#current]}`);
localStorage.setItem(Theme.#STORAGE, this.#current);
}
}
/**
* @brief
* @param {string} theme
* @returns {void}
* @throws {Error}
*/
set(theme) {
const idx = Theme.#NAME.indexOf(theme);
if (idx == -1) {
throw new Error('Invalid color sсheme.');
}
this.#set(idx);
}
toggle() {
this.#set(this.#current ? ((this.#current % (Theme.#NAME.length - 1)) + 1) : (1 + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 0 : 1)));
console.log(`Current theme: ${Theme.#NAME[this.#current]}`);
}
};
let theme = null;
let themeToggle = null;
function theme_init() {
if (!theme) {
theme = new Theme();
}
document.getElementById(Theme.#BUTTON_SVG_ID).onclick = () => {
theme.toggle();
};
window.removeEventListener('load', theme_init);
}
(function () {
window.addEventListener('load', theme_init);
}());
Итог Ссылка на заголовок
Вот собственно и всё о чём хотелось рассказать. Этот скрипт можно оптимизировать, но я намеренно дал именно такую форму вот почему:
- побуждение к размышлению;
- прикладывание рук в нужное место;
- закрепление темы алгоритмов.
Могу дать лишь небольшую подсказку:
- для автоматической темы не нужно указывать класс, браузер выберёт её самостоятельно через
@media. - для упрощения алгоритма переключения нужно будет немного изменить массив
Theme.#NAME.
В файлах умышленно отсутствует меню выбора темы, предлагаю Вам реализовать его самостоятельно. Я в Вас верю! Удачи всем, любви и обнимашек! 🤭