Создание компонента переключения темы

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

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

Размер кнопки демо увеличен для удобства просмотра.

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Веб-сайт может предоставлять настройки для управления цветовой схемой вместо того, чтобы полностью полагаться на настройки системы. Это означает, что пользователи могут просматривать сайты в режиме, отличном от их системных предпочтений. Например, система пользователя имеет светлую тему, но пользователь предпочитает, чтобы веб-сайт отображался в темной теме.

При создании этой функции необходимо учитывать несколько соображений веб-инженерии. Например, браузер должен быть уведомлен о предпочтениях как можно скорее, чтобы предотвратить мигание цвета страницы, а элемент управления должен сначала синхронизироваться с системой, а затем разрешить сохраненные на стороне клиента исключения.

На диаграмме показан предварительный просмотр событий загрузки страницы JavaScript и взаимодействия с документом, чтобы в целом показать, что существует 4 способа настройки темы.

Разметка

Для переключения следует использовать <button> кнопку>, поскольку тогда вы сможете воспользоваться преимуществами событий и функций взаимодействия, предоставляемых браузером, таких как события щелчка и возможность фокусировки.

Кнопка

Кнопке нужен класс для использования в CSS и идентификатор для использования в JavaScript. Кроме того, поскольку содержимое кнопки представляет собой значок, а не текст, добавьте атрибут title , чтобы предоставить информацию о назначении кнопки. Наконец, добавьте [aria-label] для хранения состояния кнопки со значком, чтобы программы чтения с экрана могли сообщать о состоянии темы людям с ослабленным зрением.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label и aria-live вежливые

Чтобы указать программам чтения с экрана, что изменения в aria-label должны быть объявлены, добавьте к кнопке aria-live="polite" .

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

Это добавление разметки сигнализирует программам чтения с экрана, что вместо aria-live="assertive" нужно вежливо сообщить пользователю, что изменилось. В случае с этой кнопкой она будет объявлять «светлую» или «темную» в зависимости от того, какой стала aria-label .

Значок масштабируемой векторной графики (SVG)

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

Следующая разметка SVG находится внутри <button> :

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

К элементу SVG был добавлен aria-hidden поэтому программы чтения с экрана игнорируют его, поскольку он помечен как презентационный. Это отлично подходит для визуальных украшений, например значка внутри кнопки. В дополнение к обязательному атрибуту viewBox элемента добавьте высоту и ширину по тем же причинам, по которым изображения должны иметь встроенные размеры .

Солнце

Значок солнца с солнечными лучами потускнел и появилась ярко-розовая стрелка, указывающая на круг в центре.

Изображение солнца состоит из круга и линий, для которых в SVG удобно иметь формы. <circle> центрируется путем установки для свойств cx и cy значения 12, что составляет половину размера области просмотра (24), а затем присваивается радиус ( r ) равный 6 , который устанавливает размер.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Кроме того, свойство маски указывает на идентификатор элемента SVG , который вы создадите дальше, и, наконец, задает цвет заливки, соответствующий цвету текста страницы с помощью currentColor .

Солнечные лучи

Значок солнца, центр которого исчез, и ярко-розовая стрелка, указывающая на солнечные лучи.

Затем линии солнечных лучей добавляются чуть ниже круга, внутри группового элемента <g> group.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

На этот раз вместо значения fill , равного currentColor , устанавливается обводка каждой строки. Линии и круглые формы создают красивое солнце с лучами.

Луна

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

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
Графика с тремя вертикальными слоями, показывающая, как работает маскирование. Верхний слой представляет собой белый квадрат с черным кружком. Средний слой — значок солнца. Нижний слой помечен как результат, и на нем отображается значок солнца с вырезом на месте черного круга верхнего слоя.

Маски с SVG являются мощными инструментами, позволяющими белым и черным цветам удалять или включать части другой графики. Значок солнца будет затмён формой луны <circle> с маской SVG, просто перемещая форму круга в область маски и из нее.

Что произойдет, если CSS не загрузится?

Снимок экрана: обычная кнопка браузера со значком солнца внутри.

Было бы неплохо протестировать SVG, как если бы CSS не загружался, чтобы убедиться, что результат не слишком велик и не вызывает проблем с макетом. Встроенные атрибуты высоты и ширины в SVG, а также использование currentColor дают минимальные правила стиля, которые браузер может использовать, если CSS не загружается. Это создает хорошие защитные стили против сетевой турбулентности.

Макет

Компонент переключения темы имеет небольшую площадь, поэтому вам не нужна сетка или флексбокс для макета. Вместо этого используются позиционирование SVG и преобразования CSS.

Стили

.theme-toggle стили

Элемент <button> — это контейнер для форм и стилей значков. Этот родительский контекст будет содержать адаптивные цвета и размеры для передачи в SVG.

Первая задача — сделать кнопку круглой и удалить стили кнопок по умолчанию:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Затем добавьте несколько стилей взаимодействия. Добавьте стиль курсора для пользователей мыши. Добавьте touch-action: manipulation для быстрого реагирования на прикосновения . Удаление полупрозрачного выделения iOS применяется к кнопкам. Наконец, дайте контуру состояния фокуса некоторую передышку от края элемента:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG внутри кнопки также нуждается в некоторых стилях. SVG должен соответствовать размеру кнопки и для визуальной мягкости закруглить концы строк:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Адаптивный размер с помощью медиа-запроса hover

Размер кнопки со значком немного мал — 2rem , что хорошо для пользователей мыши, но может быть проблемой для грубого указателя, такого как палец. Чтобы кнопка соответствовала многим рекомендациям по сенсорному размеру , используйте медиа-запрос при наведении, чтобы указать увеличение размера.

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

Стили SVG Солнца и Луны

Кнопка содержит интерактивные аспекты компонента переключения темы, а внутри SVG — визуальные и анимированные аспекты. Именно здесь икону можно сделать красивой и воплотить в жизнь.

Светлая тема

ALT_TEXT_ЗДЕСЬ

Чтобы анимация масштабирования и поворота происходила из центра фигур SVG, установите для них transform-origin: center center . Адаптивные цвета, предоставляемые кнопкой, используются здесь фигурами. Луна и солнце используют кнопки, предоставленные var(--icon-fill) и var(--icon-fill-hover) для заливки, а солнечные лучи используют переменные для обводки.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Темная тема

ALT_TEXT_ЗДЕСЬ

Стили луны должны удалить солнечные лучи, увеличить солнечный круг и переместить маску круга.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

Обратите внимание, что темная тема не имеет изменений цвета или переходов. Компонент родительской кнопки владеет цветами, причем они уже адаптивны в темном и светлом контексте. Информация о переходе должна находиться в основе медиа-запроса пользователя о предпочтениях движения.

Анимация

Кнопка должна быть функциональной и иметь состояние, но без переходов на этом этапе. Следующие разделы посвящены определению того, как и какие переходы.

Совместное использование медиа-запросов и импорт плавности

Чтобы упростить размещение переходов и анимации в настройках движения операционной системы пользователя, плагин PostCSS Custom Media позволяет использовать разработанную спецификацию CSS для синтаксиса переменных медиа-запроса :

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Чтобы получить уникальные и простые в использовании смягчения CSS, импортируйте часть плавности Open Props :

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Солнце

Переходы солнца будут более игривыми, чем луны, и этого эффекта можно добиться с помощью упругих плавных переходов. Солнечные лучи должны немного отскакивать при вращении, а центр солнца должен немного отскакивать при масштабировании.

Стили по умолчанию (светлая тема) определяют переходы, а стили темной темы определяют настройки перехода к светлой:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

На панели «Анимация» в Chrome DevTools вы можете найти временную шкалу для переходов анимации. Можно проверить продолжительность всей анимации, элементы и время замедления.

Переход от светлого к темному
Переход от темного к светлому

Луна

Положения лунного света и темноты уже заданы. Добавьте стили перехода внутри медиа-запроса --motionOK , чтобы оживить его, соблюдая при этом предпочтения пользователя в отношении движения.

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

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Переход от светлого к темному
Переход от темного к светлому

Предпочитает ограниченное движение

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

JavaScript

В этом компоненте много работы по JavaScript: от управления информацией ARIA для программ чтения с экрана до получения и установки значений из локального хранилища .

Опыт загрузки страницы

Было важно, чтобы при загрузке страницы не возникало мигания цвета. Если пользователь с темной цветовой схемой указывает, что он предпочитает светлую цветовую схему с этим компонентом, а затем перезагрузил страницу, сначала страница будет темной, а затем начнет светиться. Чтобы предотвратить это, нужно было выполнить небольшую блокировку JavaScript с целью как можно раньше установить data-theme атрибута HTML.

<script src="./theme-toggle.js"></script>

Для этого сначала в документе <head> загружается простой тег <script> , а затем разметка CSS или <body> . Когда браузер встречает такой немаркированный скрипт, он запускает код и выполняет его раньше остального HTML-кода. Экономно используя этот момент блокировки, можно установить атрибут HTML до того, как основной CSS отрисует страницу, тем самым предотвращая вспышку или цвета.

JavaScript сначала проверяет предпочтения пользователя в локальном хранилище и возвращается к проверке системных предпочтений, если в хранилище ничего не найдено:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Далее анализируется функция установки предпочтений пользователя в локальном хранилище:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

Далее следует функция изменения документа с настройками.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

На этом этапе важно отметить состояние анализа HTML-документа. Браузер еще не знает о кнопке «#theme-toggle», поскольку тег <head> не был полностью проанализирован. Однако в браузере есть document.firstElementChild , он же тег <html> . Функция пытается установить оба, чтобы синхронизировать их, но при первом запуске сможет установить только тег HTML. Сначала querySelector ничего не найдет, а необязательный оператор цепочки гарантирует отсутствие синтаксических ошибок, если он не найден и выполняется попытка вызова функции setAttribute.

Затем немедленно вызывается эта reflectPreference() , чтобы в HTML-документе был установлен атрибут data-theme :

reflectPreference()

Кнопке все еще нужен атрибут, поэтому дождитесь события загрузки страницы, тогда можно будет безопасно запрашивать, добавлять прослушиватели и устанавливать атрибуты:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Опыт переключения

При нажатии кнопки тему нужно поменять местами, в памяти JavaScript и в документе. Необходимо будет проверить текущее значение темы и принять решение о ее новом состоянии. Как только новое состояние будет установлено, сохраните его и обновите документ:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

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

Уникальной особенностью этого переключателя темы является синхронизация с системными настройками по мере их изменения. Если пользователь меняет свои системные настройки, когда страница и этот компонент видимы, переключатель темы изменится в соответствии с новыми предпочтениями пользователя, как если бы пользователь взаимодействовал с переключателем темы одновременно с переключением системы.

Добейтесь этого с помощью JavaScript и события matchMedia прослушивающего изменения в медиа-запросе:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Изменение системных настроек MacOS меняет состояние переключения темы.

Заключение

Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂

Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!

Ремиксы сообщества

,

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

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

Размер кнопки демо увеличен для удобства просмотра.

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Веб-сайт может предоставлять настройки для управления цветовой схемой вместо того, чтобы полностью полагаться на настройки системы. Это означает, что пользователи могут просматривать сайты в режиме, отличном от их системных предпочтений. Например, система пользователя имеет светлую тему, но пользователь предпочитает, чтобы веб-сайт отображался в темной теме.

При создании этой функции необходимо учитывать несколько соображений веб-инженерии. Например, браузер должен быть уведомлен о предпочтениях как можно скорее, чтобы предотвратить мигание цвета страницы, а элемент управления должен сначала синхронизироваться с системой, а затем разрешить сохраненные на стороне клиента исключения.

На диаграмме показан предварительный просмотр событий загрузки страницы JavaScript и взаимодействия с документом, чтобы в целом показать, что существует 4 способа настройки темы.

Разметка

Для переключения следует использовать <button> кнопку>, поскольку тогда вы сможете воспользоваться преимуществами событий и функций взаимодействия, предоставляемых браузером, таких как события щелчка и возможность фокусировки.

Кнопка

Кнопке нужен класс для использования в CSS и идентификатор для использования в JavaScript. Кроме того, поскольку содержимое кнопки представляет собой значок, а не текст, добавьте атрибут title , чтобы предоставить информацию о назначении кнопки. Наконец, добавьте [aria-label] для хранения состояния кнопки со значком, чтобы программы чтения с экрана могли сообщать о состоянии темы людям с ослабленным зрением.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label и aria-live вежливые

Чтобы указать программам чтения с экрана, что изменения в aria-label должны быть объявлены, добавьте к кнопке aria-live="polite" .

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

Это добавление разметки сигнализирует программам чтения с экрана, что вместо aria-live="assertive" нужно вежливо сообщить пользователю, что изменилось. В случае с этой кнопкой она будет объявлять «светлую» или «темную» в зависимости от того, какой стала aria-label .

Значок масштабируемой векторной графики (SVG)

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

Следующая разметка SVG находится внутри <button> :

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

К элементу SVG был добавлен aria-hidden поэтому программы чтения с экрана игнорируют его, поскольку он помечен как презентационный. Это отлично подходит для визуальных украшений, например значка внутри кнопки. В дополнение к обязательному атрибуту viewBox для элемента добавьте высоту и ширину по тем же причинам, по которым изображения должны иметь встроенные размеры .

Солнце

Значок солнца с солнечными лучами исчез, а ярко-розовая стрелка указывает на круг в центре.

Изображение солнца состоит из круга и линий, для которых в SVG удобно иметь формы. <circle> центрируется путем установки для свойств cx и cy значения 12, что составляет половину размера области просмотра (24), а затем присваивается радиус ( r ) равный 6 , который устанавливает размер.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Кроме того, свойство маски указывает на идентификатор элемента SVG , который вы создадите дальше, и, наконец, задает цвет заливки, соответствующий цвету текста страницы с помощью currentColor .

Солнечные лучи

Значок солнца, центр которого исчез, и ярко-розовая стрелка, указывающая на солнечные лучи.

Затем линии солнечных лучей добавляются чуть ниже круга, внутри группового элемента <g> group.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

На этот раз вместо значения fill , равного currentColor , устанавливается обводка каждой строки. Линии и круглые формы создают красивое солнце с лучами.

Луна

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

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
Графика с тремя вертикальными слоями, показывающая, как работает маскировка. Верхний слой представляет собой белый квадрат с черным кружком. Средний слой — значок солнца. Нижний слой помечен как результат, и на нем отображается значок солнца с вырезом на месте черного круга верхнего слоя.

Маски с SVG являются мощными инструментами, позволяющими белым и черным цветам удалять или включать части другой графики. Значок солнца будет затмён формой луны <circle> с маской SVG, просто перемещая форму круга в область маски и из нее.

Что произойдет, если CSS не загрузится?

Снимок экрана: обычная кнопка браузера со значком солнца внутри.

Было бы неплохо протестировать SVG, как если бы CSS не загружался, чтобы убедиться, что результат не слишком велик и не вызывает проблем с макетом. Встроенные атрибуты высоты и ширины в SVG, а также использование currentColor дают минимальные правила стиля, которые браузер может использовать, если CSS не загружается. Это создает хорошие защитные стили против сетевой турбулентности.

Макет

Компонент переключения темы имеет небольшую площадь, поэтому вам не нужна сетка или флексбокс для макета. Вместо этого используются позиционирование SVG и преобразования CSS.

Стили

.theme-toggle стили

Элемент <button> — это контейнер для форм и стилей значков. Этот родительский контекст будет содержать адаптивные цвета и размеры для передачи в SVG.

Первая задача — сделать кнопку круглой и удалить стили кнопок по умолчанию:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Затем добавьте несколько стилей взаимодействия. Добавьте стиль курсора для пользователей мыши. Добавьте touch-action: manipulation для быстрого реагирования на прикосновения . Удаление полупрозрачного выделения iOS применяется к кнопкам. Наконец, дайте контуру состояния фокуса некоторую передышку от края элемента:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG внутри кнопки также нуждается в некоторых стилях. SVG должен соответствовать размеру кнопки и для визуальной мягкости закруглить концы строк:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Адаптивный размер с помощью медиа-запроса hover

Размер кнопки со значком немного мал — 2rem , что хорошо для пользователей мыши, но может быть проблемой для грубого указателя, такого как палец. Чтобы кнопка соответствовала многим рекомендациям по сенсорному размеру , используйте медиа-запрос при наведении, чтобы указать увеличение размера.

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

Стили SVG Солнца и Луны

Кнопка содержит интерактивные аспекты компонента переключения темы, а SVG внутри содержит визуальные и анимированные аспекты. Именно здесь икону можно сделать красивой и воплотить в жизнь.

Светлая тема

ALT_TEXT_ЗДЕСЬ

Чтобы анимация масштабирования и поворота происходила из центра фигур SVG, установите для них transform-origin: center center . Адаптивные цвета, предоставляемые кнопкой, используются здесь фигурами. Луна и солнце используют кнопки, предоставленные var(--icon-fill) и var(--icon-fill-hover) для заливки, а солнечные лучи используют переменные для обводки.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Темная тема

ALT_TEXT_ЗДЕСЬ

Стили луны должны удалить солнечные лучи, увеличить солнечный круг и переместить маску круга.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

Обратите внимание, что темная тема не имеет изменений цвета или переходов. Компонент родительской кнопки владеет цветами, причем они уже адаптивны в темном и светлом контексте. Информация о переходе должна находиться в основе медиа-запроса пользователя о предпочтениях движения.

Анимация

Кнопка должна быть функциональной и иметь состояние, но без переходов на этом этапе. Следующие разделы посвящены определению того, как и какие переходы.

Совместное использование медиа-запросов и импорт плавности

Чтобы упростить размещение переходов и анимации в настройках движения операционной системы пользователя, плагин PostCSS Custom Media позволяет использовать разработанную спецификацию CSS для синтаксиса переменных медиа-запроса :

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Чтобы получить уникальные и простые в использовании смягчения CSS, импортируйте часть плавности Open Props :

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Солнце

Переходы солнца будут более игривыми, чем луны, и этого эффекта можно добиться с помощью упругих плавных переходов. Солнечные лучи должны немного отскакивать при вращении, а центр солнца должен немного отскакивать при масштабировании.

Стили по умолчанию (светлая тема) определяют переходы, а стили темной темы определяют настройки перехода к светлой:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

На панели «Анимация» в Chrome DevTools вы можете найти временную шкалу для переходов анимации. Можно проверить продолжительность всей анимации, элементы и время замедления.

Переход от светлого к темному
Переход от темного к светлому

Луна

Положения лунного света и темноты уже заданы. Добавьте стили перехода внутри медиа-запроса --motionOK , чтобы оживить его, соблюдая при этом предпочтения пользователя в отношении движения.

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

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Переход от светлого к темному
Переход от темного к светлому

Предпочитает ограниченное движение

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

JavaScript

В этом компоненте много работы по JavaScript: от управления информацией ARIA для программ чтения с экрана до получения и установки значений из локального хранилища .

Опыт загрузки страницы

Было важно, чтобы при загрузке страницы не происходило мигания цвета. Если пользователь с темной цветовой схемой указывает, что он предпочитает светлую цветовую схему с этим компонентом, а затем перезагрузил страницу, сначала страница будет темной, а затем начнет светиться. Чтобы предотвратить это, нужно было выполнить небольшую блокировку JavaScript с целью как можно раньше установить data-theme атрибута HTML.

<script src="./theme-toggle.js"></script>

Для этого сначала в документе <head> загружается простой тег <script> , а затем разметка CSS или <body> . Когда браузер встречает такой немаркированный скрипт, он запускает код и выполняет его раньше остального HTML-кода. Экономно используя этот момент блокировки, можно установить атрибут HTML до того, как основной CSS отрисует страницу, тем самым предотвращая вспышку или цвета.

JavaScript сначала проверяет предпочтения пользователя в локальном хранилище и возвращается к проверке системных предпочтений, если в хранилище ничего не найдено:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Далее анализируется функция установки предпочтений пользователя в локальном хранилище:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

Далее следует функция изменения документа с настройками.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

На этом этапе важно отметить состояние анализа HTML-документа. Браузер еще не знает о кнопке «#theme-toggle», поскольку тег <head> не был полностью проанализирован. Однако в браузере есть document.firstElementChild , он же тег <html> . Функция пытается установить оба, чтобы синхронизировать их, но при первом запуске сможет установить только тег HTML. Сначала querySelector ничего не найдет, а необязательный оператор цепочки гарантирует отсутствие синтаксических ошибок, если он не найден и выполняется попытка вызова функции setAttribute.

Затем немедленно вызывается функция reflectPreference() , чтобы в HTML-документе был установлен атрибут data-theme :

reflectPreference()

Кнопке все еще нужен атрибут, поэтому дождитесь события загрузки страницы, тогда можно будет безопасно запрашивать, добавлять прослушиватели и устанавливать атрибуты:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Опыт переключения

При нажатии кнопки тему нужно поменять местами, в памяти JavaScript и в документе. Необходимо будет проверить текущее значение темы и принять решение о ее новом состоянии. Как только новое состояние будет установлено, сохраните его и обновите документ:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

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

Уникальной особенностью этого переключателя темы является синхронизация с системными настройками по мере их изменения. Если пользователь меняет свои системные настройки, когда страница и этот компонент видимы, переключатель темы изменится в соответствии с новыми предпочтениями пользователя, как если бы пользователь взаимодействовал с переключателем темы одновременно с переключением системы.

Добейтесь этого с помощью JavaScript и события matchMedia прослушивающего изменения в медиа-запросе:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Изменение системных настроек MacOS меняет состояние переключения темы.

Заключение

Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂

Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!

Ремиксы сообщества

,

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

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

Размер кнопки демо увеличен для удобства просмотра.

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Веб-сайт может предоставлять настройки для управления цветовой схемой вместо того, чтобы полностью полагаться на настройки системы. Это означает, что пользователи могут просматривать сайты в режиме, отличном от их системных предпочтений. Например, система пользователя имеет светлую тему, но пользователь предпочитает, чтобы веб-сайт отображался в темной теме.

При создании этой функции необходимо учитывать несколько соображений веб-инженерии. Например, браузер должен быть уведомлен о предпочтениях как можно скорее, чтобы предотвратить мигание цвета страницы, а элемент управления должен сначала синхронизироваться с системой, а затем разрешить сохраненные на стороне клиента исключения.

На диаграмме показан предварительный просмотр событий загрузки страницы JavaScript и взаимодействия с документом, чтобы в целом показать, что существует 4 способа настройки темы.

Разметка

Для переключения следует использовать <button> кнопку>, поскольку тогда вы сможете воспользоваться преимуществами событий и функций взаимодействия, предоставляемых браузером, таких как события щелчка и возможность фокусировки.

Кнопка

Кнопке нужен класс для использования в CSS и идентификатор для использования в JavaScript. Кроме того, поскольку содержимое кнопки представляет собой значок, а не текст, добавьте атрибут title , чтобы предоставить информацию о назначении кнопки. Наконец, добавьте [aria-label] для хранения состояния кнопки со значком, чтобы программы чтения с экрана могли сообщать о состоянии темы людям с ослабленным зрением.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label и aria-live вежливые

Чтобы указать программам чтения с экрана, что изменения в aria-label должны быть объявлены, добавьте к кнопке aria-live="polite" .

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

Это добавление разметки сигнализирует программам чтения с экрана, что вместо aria-live="assertive" нужно вежливо сообщить пользователю, что изменилось. В случае с этой кнопкой она будет объявлять «светлую» или «темную» в зависимости от того, какой стала aria-label .

Значок масштабируемой векторной графики (SVG)

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

Следующая разметка SVG находится внутри <button> :

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

К элементу SVG был добавлен aria-hidden поэтому программы чтения с экрана игнорируют его, поскольку он помечен как презентационный. Это отлично подходит для визуальных украшений, например значка внутри кнопки. В дополнение к обязательному атрибуту viewBox для элемента добавьте высоту и ширину по тем же причинам, по которым изображения должны иметь встроенные размеры .

Солнце

Значок солнца с солнечными лучами исчез, а ярко-розовая стрелка указывает на круг в центре.

Изображение солнца состоит из круга и линий, для которых в SVG удобно иметь формы. <circle> центрируется путем установки для свойств cx и cy значения 12, что составляет половину размера области просмотра (24), а затем присваивается радиус ( r ) равный 6 , который устанавливает размер.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Кроме того, свойство маски указывает на идентификатор элемента SVG , который вы создадите дальше, и, наконец, задает цвет заливки, соответствующий цвету текста страницы с помощью currentColor .

Солнечные лучи

Значок солнца, центр которого исчез, и ярко-розовая стрелка, указывающая на солнечные лучи.

Затем линии солнечных лучей добавляются чуть ниже круга, внутри группового элемента <g> group.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

На этот раз вместо значения fill , равного currentColor , устанавливается обводка каждой строки. Линии и круглые формы создают красивое солнце с лучами.

Луна

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

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
Графика с тремя вертикальными слоями, показывающая, как работает маскировка. Верхний слой представляет собой белый квадрат с черным кружком. Средний слой — значок солнца. Нижний слой помечен как результат, и на нем отображается значок солнца с вырезом на месте черного круга верхнего слоя.

Маски с SVG являются мощными инструментами, позволяющими белым и черным цветам удалять или включать части другой графики. Значок солнца будет затмён формой луны <circle> с маской SVG, просто перемещая форму круга в область маски и из нее.

Что произойдет, если CSS не загрузится?

Снимок экрана: обычная кнопка браузера со значком солнца внутри.

Было бы неплохо протестировать SVG, как если бы CSS не загружался, чтобы убедиться, что результат не слишком велик и не вызывает проблем с макетом. Встроенные атрибуты высоты и ширины в SVG, а также использование currentColor дают минимальные правила стиля, которые браузер может использовать, если CSS не загружается. Это создает хорошие защитные стили против сетевой турбулентности.

Макет

Компонент переключения темы имеет небольшую площадь, поэтому вам не нужна сетка или флексбокс для макета. Вместо этого используются позиционирование SVG и преобразования CSS.

Стили

.theme-toggle стили

Элемент <button> — это контейнер для форм и стилей значков. Этот родительский контекст будет содержать адаптивные цвета и размеры для передачи в SVG.

Первая задача — сделать кнопку круглой и удалить стили кнопок по умолчанию:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Затем добавьте несколько стилей взаимодействия. Добавьте стиль курсора для пользователей мыши. Добавьте touch-action: manipulation для быстрого реагирования на прикосновения . Удаление полупрозрачного выделения iOS применяется к кнопкам. Наконец, дайте контуру состояния фокуса некоторую передышку от края элемента:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG внутри кнопки также нуждается в некоторых стилях. SVG должен соответствовать размеру кнопки и для визуальной мягкости закруглить концы строк:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Адаптивный размер с помощью медиа-запроса hover

Размер кнопки со значком немного мал — 2rem , что хорошо для пользователей мыши, но может быть проблемой для грубого указателя, такого как палец. Чтобы кнопка соответствовала многим рекомендациям по сенсорному размеру , используйте медиа-запрос при наведении, чтобы указать увеличение размера.

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

Стили SVG Солнца и Луны

Кнопка содержит интерактивные аспекты компонента переключения темы, а SVG внутри содержит визуальные и анимированные аспекты. Именно здесь икону можно сделать красивой и воплотить в жизнь.

Светлая тема

ALT_TEXT_ЗДЕСЬ

Чтобы анимация масштабирования и поворота происходила из центра фигур SVG, установите их transform-origin: center center . Адаптивные цвета, предоставляемые кнопкой, используются здесь фигурами. Луна и солнце используют кнопки, предоставленные var(--icon-fill) и var(--icon-fill-hover) для заливки, а солнечные лучи используют переменные для обводки.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Темная тема

Alt_text_here

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

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

Обратите внимание, что темная тема не имеет изменения цвета или переходов. Компонент родительской кнопки владеет цветами, где они уже адаптивны в темном и легком контексте. Информация о переходе должна стоить за запросом на предпочтение движения пользователя.

Анимация

Кнопка должна быть функциональной и состоятельной, но без переходов на данный момент. Следующие разделы посвящены определению того, как и какие переходы.

Обмен медиа -запросами и импорт смягчений

Чтобы легко установить переходы и анимации за предпочтениями движения операционной системы пользователя, плагин PostCSS Plust Media обеспечивает использование спецификации CSS для Синтаксиса переменных запросов на носитель .

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Для уникальных и простых в использовании CSS смягчения, импортируйте смягченную часть открытых реквизитов :

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Солнце

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

Стили по умолчанию (легкая тема) определяют переходы, а стили темных тем определяют настройки для перехода на свет:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

На панели анимации в Chrome Devtools вы можете найти график для анимационных переходов. Продолжительность общей анимации, элементов и срока смягчения можно проверить.

Свет к темному переходу
Темный к освещению переход

Луна

Лунный свет и темные позиции уже установлены, добавьте стили перехода внутри медиа -запроса --motionOK , чтобы воплотить его в жизнь, уважая предпочтения пользователя.

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

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Свет к темному переходу
Темный к освещению переход

Предпочитает уменьшение движения

В большинстве проблем с графическим интерфейсом я стараюсь сохранить некоторую анимацию, например, непрозрачность Cross Fades, для пользователей, которые предпочитают сокращение движения. Этот компонент чувствовал себя лучше с мгновенными изменениями состояния, однако.

JavaScript

В этом компоненте много работы для JavaScript, от управления информацией ARIA для читателей экрана до получения и настройки значений из локального хранилища .

Опыт загрузки страницы

Было важно, чтобы на загрузке страницы не произошло не было. Если пользователь с темной цветовой схемой указывает, что он предпочитал свет с этим компонентом, то перезагрузил страницу, сначала страница будет темной, то она будет мигать до освещения. Предотвращение этого означало запуск небольшого количества блокировки JavaScript с целью установить HTML-атрибут data-theme как можно раньше.

<script src="./theme-toggle.js"></script>

Чтобы достичь этого, сначала загружается простой тег <script> в документе <head> , прежде чем какая -либо CSS или <body> разметка. Когда браузер сталкивается с таким сценарием без опознавательных знаков, он запускает код и выполняет его до остальной части HTML. Используя этот блокирующий момент экономно, можно установить атрибут HTML до того, как основной CSS рисует страницу, предотвращая вспышку или цвета.

JavaScript сначала проверяет предпочтение пользователя в локальном хранении и запасении, чтобы проверить предпочтения системы, если в хранилище ничего не найдено:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Функция для установки предпочтения пользователя в локальном хранилище проанализирована дальше:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

С последующей функцией для изменения документа с предпочтениями.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

На данный момент важным является то, что на этом этапе является состояние анализа документа HTML. Браузер еще не знает о кнопке «#тематического тога», так как тег <head> не был полностью проанализирован. Тем не менее, в браузере есть document.firstElementChild . Firstelementchild, он же тег <html> . Функция пытается установить оба, чтобы сохранить их синхронизацию, но при первом запуска сможет только установить HTML -тег. Сначала querySelector ничего не найдет, и необязательный оператор цепочки не обеспечивает никаких ошибок синтаксиса, когда он не найден, и функция SetAttribute предпринимается предпринята.

Далее, эта функция reflectPreference() немедленно называется, поэтому в документе HTML есть свой набор атрибутов data-theme :

reflectPreference()

Кнопка по -прежнему нуждается в атрибуте, так что подождите событие загрузки страницы, тогда будет безопасно запросить, добавить слушателей и установить атрибуты на:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Переключение опыта

Когда кнопка нажимается, тема должна быть заменена, в памяти JavaScript и в документе. Текущее значение темы необходимо будет осмотреть и принято решение о его новом состоянии. Как только новое состояние установлено, сохраните его и обновите документ:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

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

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

Достичь этого с помощью JavaScript и событий matchMedia прослушивая изменения в медиа -запросе:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Изменение предпочтения системы MacOS изменяет состояние переключения темы

Заключение

Теперь, когда ты знаешь, как я это сделал, как бы ты 🙂

Давайте диверсифицируем наши подходы и узнаем все способы построения в Интернете. Создайте демонстрацию, напишите мне ссылки, и я добавлю ее в раздел ремиксов сообщества ниже!

Общественные ремиксы

,

Основополагающий обзор того, как создать адаптивный и доступный компонент коммутатора темы.

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

Размер демо -кнопки увеличился для легкой видимости

Если вы предпочитаете видео, вот версия этого поста YouTube:

Обзор

Веб -сайт может предоставить настройки для управления цветовой схемой, а не полностью полагаться на предпочтения системы. Это означает, что пользователи могут просматривать в режиме, отличном от своих системных предпочтений. Например, система пользователя находится в легкой теме, но пользователь предпочитает веб -сайт отображать в темной теме.

При создании этой функции есть несколько соображений веб -инженерии. Например, браузер должен быть осведомлен о предпочтениях как можно скорее, чтобы предотвратить вспышки цвета страниц, и управление необходимо сначала синхронизировать с системой, а затем разрешать исключения на стороне клиента.

Диаграмма показывает предварительный просмотр событий загрузки страницы Javascript и документа, чтобы показать 4 пути, чтобы настроить тему

Разметка

<button> Кнопка> следует использовать для переключения, так как вы затем выигрываете от предоставляемых браузером событий и функций взаимодействия, таких как события клика и фокусировка.

Кнопка

Кнопка нуждается в классе для использования из CSS и идентификатор для использования из JavaScript. Кроме того, поскольку содержание кнопки является значком, а не текстом, добавьте атрибут заголовка , чтобы предоставить информацию о цели кнопки. Наконец, добавьте [aria-label] , чтобы удержать состояние кнопки «Состояние значков», чтобы считыватели экрана могли поделиться состоянием темы для людей, которые нарушены визуально.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label и aria-live

Чтобы указать читателям экрана, что следует объявить об изменениях в aria-label , добавьте кнопку aria-live="polite" .

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

Это дополнение наценки сигнализирует о том, чтобы считывать читателей вежливо, вместо aria-live="assertive" , расскажите пользователю, что изменилось. В случае этой кнопки она объявит «свет» или «темный» в зависимости от того, чем стала aria-label .

Значок масштабируемого векторного графического (SVG)

SVG предоставляет способ создать высококачественные, масштабируемые формы с минимальной разметом. Взаимодействие с кнопкой может запустить новые визуальные состояния для векторов, что делает SVG отличным для значков.

Следующая разметка SVG заходит внутрь <button> :

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

aria-hidden была добавлена ​​в элемент SVG, чтобы читатели экрана знали, что игнорируют его как отмеченное презентация. Это здорово сделать для визуальных украшений, например, значка внутри кнопки. В дополнение к необходимому атрибуту viewBox на элементе добавьте высоту и ширину по аналогичным причинам, когда изображения должны получать встроенные размеры .

Солнце

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

Sun Graphic состоит из круга и линий, которые SVG удобно имеет формы. <circle> центрируется путем установки свойств cx и cy на 12, что составляет половину размера вида (24), а затем дает радиус ( r ) 6 , который устанавливает размер.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

Кроме того, свойство маски указывает на идентификатор элемента SVG , который вы создадите дальше, и, наконец, дали цвет заливки, который соответствует цвету текста страницы с currentColor .

Солнце лучи

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

Затем линии Sunbeam добавляются чуть ниже круга, внутри группы группы <g> .

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

На этот раз, вместо значения заполнения , который является currentColor , установлен инсульт каждой линии. Линии плюс формы круга создают хорошее солнце с балками.

Луна

Чтобы создать иллюзию бесшовного перехода между светом (солнце) и темным (луной), луна - это увеличение иконы солнца, используя маску SVG.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
График с тремя вертикальными слоями, чтобы помочь показать, как работает маскировка. Верхний слой - это белый квадрат с черным кругом. Средний слой - это солнцезащитная икона. Нижний слой помечен как результат, и он показывает значок Солнца с вырезом, где находится верхний слой черный круг.

Маски с SVG являются мощными, что позволяет цветам белым и черным либо удалять, либо включать части другой графики. Солнечный иконка будет затмеваться луной <circle> формой с маской SVG, просто перемещая форму круга в область маски.

Что произойдет, если CSS не загружается?

Экраншот простой кнопки браузера с солнечным значком внутри.

Может быть приятно проверить ваш SVG, как будто CSS не загружается, чтобы результат не был очень большим или вызывает проблемы с макетом. Встроенные атрибуты высоты и ширины на SVG плюс использование currentColor дают минимальные правила стиля для использования браузером, если CSS не загружается. Это делает приятные защитные стили против сетевой турбулентности.

Макет

Компонент переключателя темы имеет небольшую площадь поверхности, поэтому вам не нужна сетка или Flexbox для макета. Вместо этого используются позиционирование SVG и CSS -преобразования.

Стили

.theme-toggle styles

Элемент <button> - это контейнер для форм и стилей значков. Этот родительский контекст будет иметь адаптивные цвета и размеры, чтобы передать SVG.

Первая задача - сделать кнопку кругом и удалить стили кнопки по умолчанию:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

Затем добавьте несколько стилей взаимодействия. Добавьте стиль курсора для пользователей мыши. Добавьте touch-action: manipulation для быстрого реагирования . Удалите полупрозрачный выделение, которое iOS применяет к кнопкам. Наконец, дайте фокусировку наброски на контакт с передышками от края элемента:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

SVG внутри кнопки также нуждается в некоторых стилях. SVG должен соответствовать размеру кнопки и, для визуальной мягкости, завершить концы линии:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

Адаптивные размеры с помощью медиа -запроса hover

Размер кнопки значков немного мал в 2rem , что хорошо для пользователей мышей, но может быть борьбой за грубый указатель, как палец. Заставьте кнопку соответствовать многим рекомендациям по размеру сенсорного размера , используя Hover Media Requery, чтобы указать увеличение размера.

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

Sun and Moon Svg стили

Кнопка содержит интерактивные аспекты компонента коммутатора темы, в то время как SVG внутри будет удерживать визуальные и анимированные аспекты. Здесь икона может быть сделана красивой и воплощенной в жизнь.

Легкая тема

Alt_text_here

Для масштаба и повернуть анимации произойдет из центра форм SVG, установите их transform-origin: center center . Адаптивные цвета, предоставленные кнопкой, используются здесь формами. Луна и солнце используют кнопку, предоставленную var(--icon-fill) и var(--icon-fill-hover) для их заполнения, в то время как солнечные лучи используют переменные для удара.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

Темная тема

Alt_text_here

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

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

Обратите внимание, что темная тема не имеет изменения цвета или переходов. Компонент родительской кнопки владеет цветами, где они уже адаптивны в темном и легком контексте. Информация о переходе должна стоить за запросом на предпочтение движения пользователя.

Анимация

Кнопка должна быть функциональной и состоятельной, но без переходов на данный момент. Следующие разделы посвящены определению того, как и какие переходы.

Обмен медиа -запросами и импорт смягчений

Чтобы легко установить переходы и анимации за предпочтениями в операционной системе пользователя, плагин PostCSS Plust Media обеспечивает использование спецификации CSS для Синтаксиса переменных Media Query Syntax:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

Для уникальных и простых в использовании CSS смягчения, импортируйте смягченную часть открытых реквизитов :

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

Солнце

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

Стили по умолчанию (легкая тема) определяют переходы, а стили темных тем определяют настройки для перехода на свет:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

На панели анимации в Chrome Devtools вы можете найти график для анимационных переходов. Продолжительность общей анимации, элементов и срока смягчения можно проверить.

Свет к темному переходу
Темный к освещению переход

Луна

Лунный свет и темные позиции уже установлены, добавьте стили перехода внутри медиа -запроса --motionOK , чтобы воплотить его в жизнь, уважая предпочтения пользователя.

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

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
Свет к темному переходу
Темный к освещению переход

Предпочитает уменьшение движения

В большинстве проблем с графическим интерфейсом я стараюсь сохранить некоторую анимацию, например, непрозрачность Cross Fades, для пользователей, которые предпочитают сокращение движения. Этот компонент чувствовал себя лучше с мгновенными изменениями состояния, однако.

JavaScript

В этом компоненте много работы для JavaScript, от управления информацией ARIA для читателей экрана до получения и настройки значений из локального хранилища .

Опыт загрузки страницы

Было важно, чтобы на загрузке страницы не произошло не было. Если пользователь с темной цветовой схемой указывает, что он предпочитал свет с этим компонентом, то перезагрузил страницу, сначала страница будет темной, то она будет мигать до освещения. Предотвращение этого означало запуск небольшого количества блокировки JavaScript с целью установить HTML-атрибут data-theme как можно раньше.

<script src="./theme-toggle.js"></script>

Чтобы достичь этого, сначала загружается простой тег <script> в документе <head> , прежде чем какая -либо CSS или <body> разметка. Когда браузер сталкивается с таким сценарием без опознавательных знаков, он запускает код и выполняет его до остальной части HTML. Используя этот блокирующий момент экономно, можно установить атрибут HTML до того, как основной CSS рисует страницу, предотвращая вспышку или цвета.

JavaScript сначала проверяет предпочтение пользователя в локальном хранении и запасении, чтобы проверить предпочтения системы, если в хранилище ничего не найдено:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

Функция для установки предпочтения пользователя в локальном хранилище проанализирована дальше:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

С последующей функцией для изменения документа с предпочтениями.

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

На данный момент важным является то, что на этом этапе является состояние анализа документа HTML. Браузер еще не знает о кнопке «#тематического тога», так как тег <head> не был полностью проанализирован. Тем не менее, в браузере есть document.firstElementChild . Firstelementchild, он же тег <html> . Функция пытается установить оба, чтобы сохранить их синхронизацию, но при первом запуска сможет только установить HTML -тег. Сначала querySelector ничего не найдет, и необязательный оператор цепочки не обеспечивает никаких ошибок синтаксиса, когда он не найден, и функция SetAttribute предпринимается предпринята.

Далее, эта функция reflectPreference() немедленно называется, поэтому в документе HTML есть свой набор атрибутов data-theme :

reflectPreference()

Кнопка по -прежнему нуждается в атрибуте, так что подождите событие загрузки страницы, тогда будет безопасно запросить, добавить слушателей и установить атрибуты на:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

Переключение опыта

Когда кнопка нажимается, тема должна быть заменена, в памяти JavaScript и в документе. Текущее значение темы необходимо будет осмотреть и принято решение о его новом состоянии. Как только новое состояние установлено, сохраните его и обновите документ:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

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

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

Достичь этого с помощью JavaScript и событий matchMedia прослушивая изменения в медиа -запросе:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
Изменение предпочтения системы MacOS изменяет состояние переключения темы

Заключение

Теперь, когда ты знаешь, как я это сделал, как бы ты 🙂

Давайте диверсифицируем наши подходы и узнаем все способы построения в Интернете. Создайте демонстрацию, напишите мне ссылки, и я добавлю ее в раздел ремиксов сообщества ниже!

Общественные ремиксы