Создание компонента панели загрузки

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

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

Свет и тьма, неопределенность, увеличение и завершение, продемонстрированные на Chrome.

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

Обзор

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

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

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

Разметка

Я решил заключить элемент <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;
}

Скриншот devtools, на котором показан только элемент, готовый к использованию на экране.

Зона, затронутая ходом загрузки

Если у вас хорошее зрение, связать индикатор прогресса с соответствующими элементами и областями страницы может быть легко, но для пользователей с ослабленным зрением это может быть не так очевидно. Улучшите это, присвоив атрибут 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> Макет

Ширина элемента 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 DevTools будет отображать эти элементы, если вы включите этот параметр:

  1. Щелкните правой кнопкой мыши на странице и выберите «Исследовать элемент» , чтобы открыть DevTools.
  2. Нажмите на значок шестеренки «Настройки» в правом верхнем углу окна DevTools.
  3. Под заголовком Элементы найдите и установите флажок Показывать теневой DOM пользовательского агента .

Скриншот того, где в DevTools можно включить отображение теневого 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 и где найти части элемента прогресса.

Скриншот Уголка отладки, где в Safari, iOS Safari, 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 позволяет использовать псевдоэлемент: ::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> зоне

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

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> . Стоит изучить этот вопрос!

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

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

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