Тёмная тема на сайте: второй этап

Тёмная тема на сайте: второй этап

Сохраняем тему и добавляем переключатель

В прошлой статье мы добавили поддержку тёмной темы на странице. Вот короткая версия:

  • тёмная тема — это когда фон делают тёмным, а текст светлым, чтобы было удобно читать в темноте;
  • браузеры умеют определять тёмную тему на устройстве и включать её на странице, если для этого на странице есть специальная настройка в CSS;
  • вместо автоматического включения можно добавить ручной переключатель.

В прошлый раз мы проверили, как работает автопереключение, потом убрали его и добавили ручной режим. Сегодня объединим эти два подхода, а заодно научим браузер сохранять наши настройки.

Добавляем автопереключатель на страницу

Добавим автопереключение тёмной темы на страницу следующим способом:

  1. Мы оставляем ручной выбор темы на случай, если пользователь захочет сам выбрать, что ему удобнее.
  2. Ниже добавляем переключатель авторежима тёмной темы — когда он включён, используются системные настройки.
  3. После включения авторежима ручной выбор будет недоступен — мы его скроем с экрана. 
  4. После выключения ручной выбор снова становится доступен.

Чтобы не городить с нуля элементы интерфейса, мы позаимствуем готовые компоненты из фреймворка Bootstrap 5. Оттуда нам нужен переключатель-слайдер. Мы вставим его в HTML-страницу после абзаца с ручным переключением. Сразу добавим вызов функции autoDarkLight() при нажатии на переключатель:

<div class="form-check form-switch">
  <input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault" onclick="autoDarkLight()" >
  <label class="form-check-label" for="flexSwitchCheckDefault">Автонастройка</label>
</div>
Тёмная тема на сайте: второй этап
Переключатель появился, но он пока ни на что не влияет

Настраиваем авторежим

Мы уже выяснили, что для автоматического включения тёмной темы достаточно добавить в стили такой медиазапрос:

@media screen and (prefers-color-scheme: dark) {
  body {
    background: black;
    color: white;
      }
}

Нам нужно добавить это в стили при включении авторежима и убрать оттуда при его выключении. Чтобы получить доступ к таблице стилей, добавим в  самое начало скрипта dark.js такое:

// находим стили по тегу
const style = document.getElementsByTagName("style")[0];
// получаем доступ к разделу стилей
const styleSheet = style.sheet;

Теперь наша задача — написать функцию autoDarkLight()  и добавить в неё всего одну проверку: нажат переключатель или нет. Если нажат — добавляем медиазапрос в стили, а если не нажат — убираем его оттуда.

Хитрость в том, что медиазапрос добавится в самый конец стилей, а значит, он применится самым последним. Получается, нам неважно, какие стили у страницы стояли до этого, — всё равно последним сработает наш автовыбор тёмной темы.

// переменная для временного хранения индекса медиазапроса
var temp;

function autoDarkLight() {
	// если переключатель установлен
	if (flag.checked) {
		// создаём новый медиазапрос
		const mediaRuleText = `@media screen and (prefers-color-scheme: dark) {
		  body {
		    background: black;
		    color: white;
		      }
		}`;
		// добавляем его на страницу
		const mediaRuleIndex = styleSheet.insertRule(mediaRuleText);
		// сохраняем индекс запроса в стилях
		temp = mediaRuleIndex;
		// скрываем ручной переключатель
		p.style.visibility = "hidden";
	// если переключатель не установлен
	} else {
		// удаляем медиазапрос по индексу
		styleSheet.deleteRule(temp);
		// показываем ручной переключатель
		p.style.visibility = "visible";
	}

}
Тёмная тема на сайте: второй этап
Переключатель скрывает ручной выбор и добавляет поддержку выбранной темы устройства

Сохраняем настройки

Страница теперь умеет переключаться в тёмный режим и автоматически, и вручную, но при перезагрузке всё слетает — все режимы нужно заново включать самому.

Чтобы сайт запоминал, что мы сделали в настройках, используем локальное хранилище браузера (оно же localStorage). Логика будет такая:

  • При каждом переключении мы записываем в хранилище текущее значение переключателя. Если в памяти нет вообще никакого значения, значит, мы ещё не трогали этот переключатель.
  • Когда страница загрузилась, мы достаём из памяти по очереди значение каждого переключателя и применяем их на страницу.

Применяем — это значит, что мы виртуально нажимаем на переключатели, если они активны. Это позволяет сразу установить всё в то положение, которое было до перезагрузки. Читайте комментарии, чтобы разобраться в логике работы с хранилищем:

// выполняется сразу после загрузки страницы
window.addEventListener('DOMContentLoaded', (event) => { 
  // если в нашем хранилище есть информация о том, что мы установили вручную тёмную тему
  if (localStorage.getItem('selected') == 'true') {
  	// виртуально нажимаем на ручной переключатель темы
  	darkLight();
	// если такой информации там нет — 
	} else {
		// сохраняем текущее значение темы
		localStorage.setItem('selected',dark);
	}

  // если в нашем хранилище есть информация о том, что мы установили авторежим
  if (localStorage.getItem('auto') == 'true') {
  	// виртуально включаем переключатель авторежима
  	flag.checked = true;
  	// вызываем обработчик автопереключателя
  	autoDarkLight();
  // если такой информации там нет
	} else {
		// значит, мы просто сохраняем значение false – авторежим выключен
		localStorage.setItem('auto',false);
	}
});

Последнее, что нам осталось сделать, — сохранять в память текущее положение переключателей во время их изменения. Для этого добавим в самый конец функции darkLight() такую строку:

// сохраняем в памяти статус ручного переключателя
localStorage.setItem('selected',dark);

И то же самое сделаем в функции autoDarkLight() — добавим сохранение после установки видимости ручного переключателя:

localStorage.setItem('auto',true); ← если переключатель установлен;

localStorage.setItem('auto',false); ← если переключатель не установлен.

Результат

У нас получился красивый переключатель, который заставляет страницу слушаться системных настроек. 

Также мы научились хранить состояние переключателей на устройстве. 

Всё это живёт на нашей колхозной странице, на которую мы с каждым новым проектом добавляем всё больше элементов. Однажды она будет похожа на лавку торговца на турецком «Гранд базаре», а пока вот результат: 

Посмотреть работу переключателей на странице проекта

Из интересного в этом проекте — симпатичный интерфейсный компонент в Bootstrap 5. В одной из будущих статей разберём, что там вообще интересного подвезли. 

<!DOCTYPE html>
<html>
<!-- служебная часть -->
<head>
    
    
    
    
  <!-- заголовок страницы -->
  <title>Михаил Максимов — преподаватель информатики</title>
  <!-- настраиваем служебную информацию для браузеров -->
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- загружаем Бутстрап -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
  <style type="text/css">
  

    .theme-dark {
      background: black;
      color: white;
    }

    .theme-light {
      background: white;
      color: black;
    }


    img{ 
      max-width: 100%; 
      max-height: 100%;
    } 

    h1{
      font-size:50px;
      margin-top: 30px;
      margin-bottom: 20px;
    } 

    h2{
      margin-top: 40px;
      margin-bottom: 20px;
    }

    p {
      font-size: 18px;
    } 

    .switch {
      cursor: help;
    }

  </style>
<!-- Yandex.Metrika counter -->
    <script type="text/javascript" >
    (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
        m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
        (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");

       ym(80335906, "init", {
            clickmap:true,
            trackLinks:true,
            accurateTrackBounce:true
    });
    </script>
    <noscript><div><img src="https://mc.yandex.ru/watch/80335906" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
    <!-- /Yandex.Metrika counter -->
    
    
   <!-- Put this script tag to the <head> of your page -->
    <script type="text/javascript" src="https://vk.com/js/api/openapi.js?169"></script>
    
    <script type="text/javascript">
      VK.init({apiId: 7836404, onlyWidgets: true});
    </script>
</head>

<body id="main">
    

  <div class="container" >
    <div class="row">
      <div class="col-12">
          
            <div class="ya-site-form ya-site-form_inited_no" data-bem="{"action":"https://yandex.ru/search/site/","arrow":false,"bg":"transparent","fontsize":12,"fg":"#000000","language":"ru","logo":"rb","publicname":"Найти на сайте","suggest":true,"target":"_self","tld":"ru","type":2,"usebigdictionary":true,"searchid":2532273,"input_fg":"#000000","input_bg":"#ffffff","input_fontStyle":"normal","input_fontWeight":"normal","input_placeholder":"Поиск по сайту","input_placeholderColor":"#000000","input_borderColor":"#7f9db9"}"><form action="https://yandex.ru/search/site/" method="get" target="_self" accept-charset="utf-8"><input type="hidden" name="searchid" value="2532273"/><input type="hidden" name="l10n" value="ru"/><input type="hidden" name="reqenc" value=""/><input type="search" name="text" value=""/><input type="submit" value="Найти"/></form></div><style type="text/css">.ya-page_js_yes .ya-site-form_inited_no { display: none; }</style><script type="text/javascript">(function(w,d,c){var s=d.createElement('script'),h=d.getElementsByTagName('script')[0],e=d.documentElement;if((' '+e.className+' ').indexOf(' ya-page_js_yes ')===-1){e.className+=' ya-page_js_yes';}s.type='text/javascript';s.async=true;s.charset='utf-8';s.src=(d.location.protocol==='https:'?'https:':'http:')+'//site.yandex.net/v2.0/js/all.js';h.parentNode.insertBefore(s,h);(w[c]||(w[c]=[])).push(function(){Ya.Site.Form.init()})})(window,document,'yandex_site_callbacks');</script>

        <p id="select" onclick="darkLight()" style="cursor: help;">Включить тёмную тему</p>
        
        <div class="form-check form-switch">
          <input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault" onclick="autoDarkLight()" >
          <label class="form-check-label" for="flexSwitchCheckDefault">Автонастройка</label>
        </div>


        <h1>Михаил Максимов</h1>
      </div>
    </div>
  </div>

  <div class="container" >
    <div class="row">
      <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
        <p>Я преподаю информатику с 2008 года, когда предмет ещё назывался ИКТ. Начинал со школы, учил детей разбираться в программировании и сдавать ЕГЭ на 90 баллов и выше. За два года вывел нашу школу на второе место в районе по олимпиадам по информатике. Вёл два класса коррекции — пятый и одиннадцатый — и знаю, как объяснить основы теории вероятности даже тем, кто не хочет ничему учиться.</p>
        <p>В 2012 защитил кандидатскую диссертацию по обучению информатике детей с недостатком внимания и стал внештатным преподавателем РГСУМ им. Макаренко. Параллельно с этим веду курсы по программированию «IDDQD» и записываю подкаст «Прогрокаст» с аудиторией 25 000 человек.</p>
      </div>
      <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
        <img src="sq_me.jpg" >
      </div>
    </div>
  </div>

  <div class="container" >
    <div class="row">
      <div class="col-12">
        <h2>Мои научные работы</h2>
      </div>
    </div>
  </div>
  
 


  <div class="container">
    <div class="row">

      <div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3" >
        <p><a href="https://thecode.media/baboolya/">Задача про бабушку и помидоры</a></p>
        <p><a href="https://thecode.media/electrician/">Хитрый электрик</a></p>
      </div> 

      <div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
        <p><a href="https://thecode.media/le-timer/">Как сделать свой таймер-напоминалку</a></p>  
        <p><a href="https://thecode.media/sublime-one-love/">Почему Sublime Text — это круто</a></p>
      </div>

      <div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
        <p><a href="https://thecode.media/est-tri-shkatulki/">Поговорим о Якубовиче</a></p>
        <p><a href="https://thecode.media/content-manager/">Как стать контент-менеджером</a></p>
      </div>  

      <div class="col-12 col-sm-12 col-md-6 col-lg-3 col-xl-3">
        <p><a href="https://thecode.media/batareyki-besyat/">Задача про сторожа и фонарик</a></p>
        <p><a href="https://thecode.media/variables/">О названиях функций</a></p>
       
      </div>  

    </div>
  </div> 

  <div class="container" >
    <div class="row">
      <div class="col-12">
        <h2>Контакты для связи</h2>
      </div>
    </div>
  </div>

  <div class="container" >
    <div class="row">
      <div class="col-12">
        <p>Телефон: +7 (123) 456-78-90</p>
        <p>Почта: <a href="mailto: mihailmaximov@gmail.com">mikemaximov@gmail.com</a></p>
        <p>Скайп: mihailmaximov</p>
        <p>Телеграм: @mihailmaximov</p>
      </div>
    </div>
  </div>
  

<!-- Put this div tag to the place, where the Comments block will be -->
    <div id="vk_comments"></div>
    <script type="text/javascript">
    VK.Widgets.Comments("vk_comments", {limit: 10, attach: "*"});
    </script>

    <script type="text/javascript" src="dark.js"></script>


</body>
<!-- конец всей страницы -->
</html>

// на старте тёмная тема не установлена
var dark = false;
// получаем доступ ко всей странице и к абзацу с переключателем
var a = document.body;
var p = document.getElementById("select")

// находим стили по тегу
const style = document.getElementsByTagName("style")[0];
// получаем доступ к разделу стилей
const styleSheet = style.sheet;

var flag = document.getElementById("flexSwitchCheckDefault");



// эта функция будет срабатывать при нажатии на переключатель
function darkLight() {
	// если тёмная тема не активна
	if (!dark) {
		// добавляем класс с тёмной темой ко всей странице
		a.className = "theme-dark";
		// меняем надпись на переключателе
		p.innerHTML = "Включить светлую тему";
	// а если активна — 
	} else {
		// добавляем класс со светлой темой ко всей странице
		a.className = "theme-light";
		// меняем надпись на переключателе
		p.innerHTML = "Включить тёмную тему";
	}

	// меняем значение темы на противоположное
	dark = !dark;
	// сохраняем в памяти статус ручного переключателя
	localStorage.setItem('selected',dark);
}

// переменная для временного хранения индекса медиазапроса
var temp;

function autoDarkLight() {
	// если переключатель установлен
	if (flag.checked) {
		// создаём новый медиазапрос
		const mediaRuleText = `@media screen and (prefers-color-scheme: dark) {
		  body {
		    background: black;
		    color: white;
		      }
		}`;
		// добавляем его на страницу
		const mediaRuleIndex = styleSheet.insertRule(mediaRuleText);
		// сохраняем индекс запроса в стилях
		temp = mediaRuleIndex;
		// скрываем ручной переключатель
		p.style.visibility = "hidden";
		// сохраняем положение переключателя в памяти браузера
		localStorage.setItem('auto',true);
	// если переключатель не установлен
	} else {
		// удаляем медиазапрос по индексу
		styleSheet.deleteRule(temp);
		// показываем ручной переключатель
		p.style.visibility = "visible";
		// сохраняем положение переключателя в памяти браузера
		localStorage.setItem('auto',false);
	}

}

// выполняется сразу после загрузки страницы
window.addEventListener('DOMContentLoaded', (event) => { 
  // если в нашем хранилище есть информация о том, что мы установили вручную тёмную тему
  if (localStorage.getItem('selected') == 'true') {
  	// виртуально нажимаем на ручной переключатель темы
  	darkLight();
	// если такой информации там нет — 
	} else {
		// сохраняем текущее значение темы
		localStorage.setItem('selected',dark);
	}

  // если в нашем хранилище есть информация о том, что мы установили авторежим
  if (localStorage.getItem('auto') == 'true') {
  	// виртуально включаем переключатель авторежима
  	flag.checked = true;
  	// вызываем обработчик автопереключателя
  	autoDarkLight();
  // если такой информации там нет
	} else {
		// значит мы просто сохраняем значение false – авторежим выключен
		localStorage.setItem('auto',false);
	}
});

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Веб-разработка — это новый чёрный
На базе веб-технологий делают всё — от сложного софта до высокобюджетных игр. Изучите технологии и начните карьеру в ИТ. Старт бесплатно. Попробуйте, вдруг вам понравится.
Изучить
Веб-разработка — это новый чёрный
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
medium