Как создать компонент индикатора загрузки

Базовый обзор того, как создать адаптивную по цвету и доступную полосу загрузки с помощью элемента <progress>.

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

Светлая и тёмная темы, неопределенный, увеличивающийся и завершенный индикаторы в Chrome.

Если вы предпочитаете видео, посмотрите ролик на YouTube:

Обзор

Элемент <progress> предоставляет пользователям визуальную и звуковую обратную связь о завершении. Такая визуальная обратная связь полезна в таких сценариях, как: заполнение формы, отображение информации о загрузке или выгрузке или даже показ того, что объем прогресса неизвестен, но работа все ещё активна.

В этом GUI Challenge мы использовали существующий HTML-элемент <progress>, чтобы упростить работу со специальными возможностями. Цвета и макеты расширяют возможности настройки встроенного элемента, модернизируют компонент и улучшают его интеграцию в системы проектирования.

Светлые и темные вкладки в каждом браузере, на которых показан адаптивный значок. Сверху вниз: Safari, Firefox, Chrome.
Демонстрация в Firefox, Safari, Safari для iOS, Chrome и Chrome для Android в светлой и темной темах.

Markup

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

<progress></progress>

Если атрибут value отсутствует, то прогресс элемента не определен. По умолчанию атрибут max равен 1, поэтому прогресс находится в диапазоне от 0 до 1. Например, если для параметра max задать значение 100, то диапазон будет от 0 до 100. Я решил остаться в пределах от 0 до 1, преобразовав значения прогресса в 0,5 или 50%.

Прогресс с ярлыком

При неявной связи элемент индикатора выполнения заключен в ярлык, например:

<label>Loading progress<progress></progress></label>

В своем примере я выбрал ярлык Только для программ чтения с экрана. Для этого нужно заключить текст ярлыка в тег <span> и применить к нему стили, которые скроют его с экрана:

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

и следующим кодом CSS от WebAIM:

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

Скриншот инструментов разработчика, на котором показан элемент screen-ready-only.

Область, затронутая ходом загрузки

Если у вас хорошее зрение, вам может быть легко связать индикатор выполнения со связанными элементами и областями страницы, но для пользователей с нарушениями зрения это не так очевидно. Чтобы улучшить этот показатель, назначьте атрибут aria-busy самому верхнему элементу, который изменится после завершения загрузки. Кроме того, укажите связь между индикатором выполнения и зоной загрузки с помощью атрибута aria-describedby.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

В JavaScript переключите aria-busy на true в начале задачи и на false после ее завершения.

Добавление атрибутов ARIA

Хотя неявная роль элемента <progress> – progressbar, я сделал ее явной для браузеров, в которых нет этой неявной роли. Я также добавил атрибут indeterminate, чтобы явно перевести элемент в неизвестное состояние. Это понятнее, чем просто отсутствие атрибута value.

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

Используйте tabindex="-1", чтобы сделать элемент прогресса доступным для фокусировки из JavaScript. Это важно для программ чтения с экрана, поскольку, когда фокус перемещается по мере изменения прогресса, пользователь узнает, насколько далеко продвинулся обновленный прогресс.

Стили

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

Макет

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

Макет "<progress>"

Ширина элемента индикатора выполнения не задана, поэтому он может уменьшаться и увеличиваться в зависимости от доступного пространства. Встроенные стили удаляются путем присвоения appearance и border значения none. Это делается для того, чтобы элемент можно было нормализовать в разных браузерах, поскольку у каждого браузера есть свои собственные стили для своего элемента.

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

Значение 1e3px для _radius использует научную нотацию для выражения большого числа, поэтому border-radius всегда округляется. Это эквивалентно 1000px. Мне нравится использовать это значение, потому что оно достаточно велико, чтобы задать его и забыть (и его короче писать, чем 1000px). При необходимости его легко увеличить: просто замените 3 на 4, и 1e4px будет эквивалентно 10000px.

overflow: hidden используется, но вызывает споры. Это упростило некоторые вещи, например, не нужно было передавать значения border-radius в дорожку и отслеживать элементы заполнения; но это также означало, что никакие дочерние элементы прогресса не могли жить за пределами элемента. Другая итерация этого пользовательского элемента прогресса может быть выполнена без overflow: hidden, и это может открыть некоторые возможности для анимации или лучшего состояния завершения.

Готово

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

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

Скриншот индикатора загрузки, который показывает 100 % и галочку в конце.

Цвет

Браузер использует собственные цвета для элемента прогресса и адаптируется к светлым и темным цветам с помощью всего одного свойства CSS. Это можно дополнить некоторыми специальными селекторами для браузера.

Светлая и тёмная темы браузера

Чтобы включить на сайте адаптивный элемент <progress>, достаточно добавить color-scheme.

progress {
  color-scheme: light dark;
}

Цвет заполнения индикатора выполнения для одного свойства

Чтобы изменить оттенок элемента <progress>, используйте accent-color.

progress {
  accent-color: rebeccapurple;
}

Обратите внимание, что цвет фона дорожки меняется со светлого на темный в зависимости от accent-color. Браузер обеспечивает правильный контраст: довольно аккуратно.

Полностью настраиваемые светлые и темные цвета

Задайте два дополнительных свойства для элемента <progress>: одно для цвета дорожки, а другое – для цвета индикатора выполнения. Внутри запроса медиа prefers-color-scheme укажите новые значения цвета для дорожки и индикатора воспроизведения.

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

Стили фокуса

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

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

Скриншот индикатора загрузки с кольцом фокуса вокруг него. Цвета совпадают.

Пользовательские стили в разных браузерах

Настройте стили, выбрав части элемента <progress>, которые показывает каждый браузер. Элемент progress – это один тег, но он состоит из нескольких дочерних элементов, которые представлены псевдоселекторами CSS. Чтобы увидеть эти элементы в инструментах разработчика Chrome, включите следующий параметр:

  1. Нажмите правой кнопкой мыши на страницу и выберите Просмотреть код, чтобы открыть Инструменты разработчика.
  2. Нажмите на значок настроек в правом верхнем углу окна инструментов разработчика.
  3. В разделе Elements (Элементы) установите флажок Show user agent shadow DOM (Показывать теневую модель DOM агента пользователя).

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

Стили Safari и Chromium

Браузеры на основе WebKit, такие как Safari и Chromium, поддерживают ::-webkit-progress-bar и ::-webkit-progress-value, что позволяет использовать подмножество CSS. Пока что задайте background-color, используя созданные ранее дополнительные параметры, которые адаптируются к светлой и темной темам.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

Скриншот, на котором показаны внутренние элементы индикатора выполнения.

Стили Firefox

Firefox предоставляет псевдоселектор ::-moz-progress-bar только для элемента <progress>. Это также означает, что мы не можем напрямую окрашивать дорожку.

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

Скриншот Firefox с указанием, где найти части элемента прогресса.

Скриншот раздела Debugging Corner (Уголок отладки), на котором показано, что индикатор загрузки работает в Safari, Safari для iOS, Firefox, Chrome и Chrome для Android.

Обратите внимание, что в Firefox цвет дорожки задан в accent-color, а в iOS Safari дорожка светло-синяя. То же самое и в тёмной теме: в Firefox полоса прокрутки тёмная, но не того цвета, который мы задали, а в браузерах на основе Webkit всё работает.

Анимация

При работе со встроенными псевдоселекторами браузера часто используется ограниченный набор разрешенных свойств CSS.

Анимация заполнения дорожки

Добавление перехода к inline-size элемента progress работает для Chromium, но не для Safari. В Firefox также не используется свойство transition для ::-moz-progress-bar.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

Анимация состояния :indeterminate

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

Дополнительные параметры

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

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

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

Ключевые кадры

Цель – создать бесконечную анимацию, которая воспроизводится вперед и назад. Начальный и конечный ключевые кадры будут заданы в CSS. Чтобы создать анимацию, которая будет возвращаться к началу снова и снова, нужен только один ключевой кадр – средний, на отметке 50%.

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

Таргетинг на каждый браузер

Не все браузеры позволяют создавать псевдоэлементы в самом элементе <progress> или анимировать индикатор выполнения. Анимацию дорожки поддерживает больше браузеров, чем анимацию псевдоэлемента, поэтому я перехожу от псевдоэлементов к анимированным полосам.

Псевдоэлемент Chromium

В Chromium разрешено использовать псевдоэлемент ::after с позиционированием, чтобы перекрыть элемент. Используются неопределенные настраиваемые свойства, и анимация туда-сюда работает очень хорошо.

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Индикатор выполнения в Safari

Для Safari дополнительные параметры и анимация применяются к псевдоэлементу индикатора выполнения:

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Индикатор выполнения Firefox

Для Firefox дополнительные параметры и анимация также применяются к псевдоэлементу индикатора выполнения:

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

JavaScript играет важную роль в работе элемента <progress>. Он управляет значением, отправляемым элементу, и обеспечивает наличие в документе достаточного количества информации для программ чтения с экрана.

const state = {
  val: null
}

В демоверсии есть кнопки для управления ходом выполнения. Они обновляют state.val, а затем вызывают функцию для обновления DOM.

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

В этой функции происходит оркестровка UI/UX. Для начала создайте функцию setProgress(). Параметры не требуются, поскольку у функции есть доступ к объекту state, элементу progress и зоне <main>.

const setProgress = () => {
  
}

Настройка статуса загрузки в зоне <main>

В зависимости от того, завершен ли процесс, нужно обновить атрибут aria-busy связанного элемента <main>.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

Очистить атрибуты, если сумма загрузки неизвестна

Если значение неизвестно или не задано, null в этом случае удалите атрибуты value и aria-valuenow. Это приведет к тому, что <progress> станет неопределенным.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

Как устранить проблемы с десятичными числами в JavaScript

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

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

Округлите значение, чтобы оно было понятным и читаемым:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

Задайте значение для программ чтения с экрана и состояния браузера

Значение используется в трех местах в DOM:

  1. Атрибут value элемента <progress>.
  2. Атрибут aria-valuenow.
  3. Внутренний текстовый контент <progress>.
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

Фокус на прогрессе

После обновления значений зрячие пользователи увидят изменение прогресса, но пользователи программ чтения с экрана пока не получат уведомление об изменении. Установите фокус на элементе <progress>, и браузер сообщит об обновлении.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

Скриншот приложения Voice Over для Mac OS, которое озвучивает пользователю прогресс на индикаторе загрузки.

Заключение

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

Если бы у меня был ещё один шанс, я бы кое-что изменил. Я думаю, что есть возможность очистить текущий компонент и попробовать создать его без ограничений стиля псевдокласса элемента <progress>. Стоит попробовать!

Давайте разнообразим наши подходы и изучим все способы создания веб-сайтов.

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

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