
Разработка индикатора прогресса — одна из самых простых задач для фронтенд-разработчика. Все, что вам нужно, это базовые знания HTML и CSS. JavaScript используется только для расчета процента выполнения задачи.
Однако эта простота обманчива. Интернет кишит множеством решений сообщества, предлагающих создать индикатор прогресса с вложенными div-контейнерами и CSS, чтобы оживить ситуацию. Держитесь подальше от этих решений! Это почти преступление против человечества, поскольку они ухудшают семантику шаблона и нарушают доступность.
В этой статье я покажу, как в Taiga UI мы разрабатывали компоненты Angular: ProgressBar и ProgressCircle.
Разработка ProgressBar
Формирование задачи
Имеется встроенный HTML-тег ‹прогресс /›. МДН утверждает:
HTML-элемент ‹progress› отображает индикатор, показывающий ход выполнения задачи, обычно отображаемый в виде индикатора выполнения.
Важно то, что каждый браузер по-разному отображает этот HTML-тег. Нам нужен такой индикатор прогресса, который выглядит одинаково во всех браузерах.
Неправильное решение/неправильное решение
Первая мысль, которая приходит в голову, может быть поверхностной: «Давайте вообще не будем использовать встроенный тег HTML. Это не красиво и выглядит по-разному в каждом браузере». Эта идея культивирует еще одно неправильное решение в сообществе. Предложение вращается вокруг создания двух <div/>-контейнеров: первый контейнер — это трекер прогресса, а второй — индикатор прогресса. Следующим шагом является настройка контейнеров и изменение ширины индикатора выполнения с помощью JavaScript (от 0 до 100%).
Упрощенная версия этого неправильного решения следующая. Файл HTML содержит два упомянутых контейнера:
<div class="track"> <div class="indicator"></div> </div>
CSS-файл:
.track {
position: relative;
background-color: grey;
width: 300px;
height: 20px;
}
.indicator {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 50%; /* change it via JS */
background-color: yellow;
}
Это работает визуально. Но это только визуально. Однако что, если пользователь с ослабленным зрением (например, не различающий цвета или слепой) решит посетить веб-сайт и не увидит индикатор выполнения?
В таких случаях люди используют программы для чтения с экрана. Эти программы зачитывают пользователю вслух все важное, что происходит на экране. Тем не менее, в случае решения «div» программа чтения с экрана не сможет понять этот тип индикатора прогресса. Он «увидит» только два <div />-контейнера, залитых разными цветами, и не озвучит их пользователю.
Это предлагаемое решение не только ухудшает семантику HTML, но и нарушает доступность нашего веб-приложения. Конечно, мы можем помочь скринридеру обнаружить индикатор прогресса: мы можем установить role="progressbar", а также добавить aria-valuenow, aria-valuemax и aria-valuemin во внешний контейнер. Однако не нужно изобретать велосипед и усложнять базовые понятия!
Улучшение решения DIV
Существует популярный подход, который может исправить доступность предыдущего решения. Мы можем поместить визуально скрытый собственный HTML-тег <progress /> внутри нашего пользовательского компонента прогресса. Затем мы должны передать ему атрибуты value и max, а все остальное оставить как было в предыдущем решении.
Существует несколько подходов к визуальному скрытию HTML-контейнера, но оставлению его обнаруживаемым для программ чтения с экрана. Обычно это sr-only CSS-класс. Например, такие классы есть в таких популярных CSS-фреймворках, как Bootstrap и Tailwind CSS.
Внимание! Не используйте
display: none,height: 0иwidth: 0. Они скрывают контент не только визуально, но и для программ чтения с экрана. Подробнее об этом читайте в статье «CSS в действии: невидимый контент только для пользователей программ чтения с экрана».
После него индикатор прогресса не только визуально приятен, но и не нарушает принцип доступности. Например, встроенный в Mac OS скринридер произносит это как «n-процентный индикатор прогресса» (где n — 100 × value / max).
Несмотря на прогресс, мы все еще недовольны семантикой шаблона! Кроме того, мы должны создать много @Input()-свойств в нашем компоненте, чтобы передать все необходимые атрибуты внедренному нативному <progress />-тегу без каких-либо мутаций. Пока это только value и max, и кто знает, может завтра нам нужно будет добавить id или один из data-* атрибутов. В результате наш компонент несет бесполезные атрибуты!
Наше решение с атрибутивным компонентом Angular
Мы предлагаем решение, не требующее дополнительных тегов HTML. Все, что нам нужно сделать, это расширить родной элемент <progress />. Всякий раз, когда мы расширяем любой нативный элемент HTML, официальная документация Angular рекомендует создавать компонент, который использует селектор атрибутов с этим элементом. Эта практика активно используется в нашей библиотеке UI-Kit Taiga: например, компоненты кнопка, ссылка и ярлык.
Давайте создадим файл TypeScript с нашим компонентом атрибутов:
import {
ChangeDetectionStrategy,
Component,
HostBinding,
Input
} from '@angular/core';
@Component({
selector: 'progress[tuiProgressBar]',
template: '',
styleUrls: ['./progress-bar.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TuiProgressBarComponent {
@Input()
@HostBinding('style.--tui-progress-color')
color?: string;
}
Файл Less включает в себя несколько less-mixins.
Следующий удаляет все встроенные настройки из браузера:
.clearProgress() {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
}
И это помогает настроить трекер прогресса:
.progressTrack(@property, @value) {
@{property}: @value; // Edge | Mozilla
&::-webkit-progress-bar {
@{property}: @value; // Chrome | Opera | Safari
}
}
Последний миксин помогает настроить цвет индикатора прогресса:
.progressIndicatorColor(@color) {
color: @color; // Not Chromium Edge
&::-webkit-progress-value {
background: @color; // Chromium Edge | Chrome | Opera | Safari
}
&::-moz-progress-bar {
background: @color; // Mozilla
}
}
Наконец, примените все созданные миксины к :host-элементу нашего компонента (напоминаю: это нативный <progress />, к которому был применен наш атрибут-компонент).
:host {
.clearProgress();
.progressIndicatorColor(var(--tui-progress-color, currentColor));
.progressTrack(background-color, grey);
color: yellow;
}
Вот и все! Мы разработали хороший компонент атрибута Angular, который обернут вокруг собственного элемента <progress />. Цвет этого индикатора можно задать через свойство CSS color или через свойство ввода компонента (например, если мы хотим создать сложный цвет градиента). В коде компонент объявляется следующим образом:
<progress tuiProgressBar value="60" max="100"></progress>
Вы можете увидеть компонент в действии на витрине нашего проекта Taiga UI. Окончательный исходный код доступен на GitHub.
Развитие ProgressCircle
К сожалению, создание компонента ProgressCircle только из нативного <progress />-элемента невозможно. Нам нужны дополнительные теги шаблона.
И здесь дело обстоит так же, любопытные интернет-пользователи могут наткнуться на различные несерьёзные решения, предлагающие создать круговой индикатор прогресса из div-контейнеров, округлённых с помощью border-radius: 50%. Также такие решения используют значительное количество JavaScript.
Но так как мы в "клубе бездивов", то эту задачу можно решить и без обращения к div-ам. Мы будем использовать svg-тег <circle />. Более того, мы будем использовать как можно меньше JavaScript. В этом нам помогают препроцессор Less и переменные CSS.
Создадим TypeScript файл нашего будущего компонента:
import {
ChangeDetectionStrategy,
Component,
HostBinding,
Input,
} from '@angular/core';
@Component({
selector: 'tui-progress-circle',
templateUrl: './progress-circle.template.html',
styleUrls: ['./progress-circle.style.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TuiProgressCircleComponent {
@Input()
value = 0;
@Input()
max = 1;
@Input()
@HostBinding('style.--tui-progress-color')
color: string | null = null;
@Input()
@HostBinding('attr.data-size')
size: 'm' | 'l' = 'm';
@HostBinding('style.--progress-percentage')
get progressPercentage(): number {
return this.value / this.max;
}
}
HTML-файл содержит:
<progress
class="hidden-progress"
[value]="value"
[max]="max"
></progress>
<svg class="svg" height="100%" width="100%" aria-hidden="true">
<circle
class="track"
cx="50%"
cy="50%"
></circle>
<circle
class="progress"
cx="50%"
cy="50%"
></circle>
</svg>
И последний шаг — создание LESS-файла. Мы будем использовать многие возможности Less: примеси, Карты и встроенные Математические функции.
Во-первых, давайте создадим константы карты, которые хранят значения для разных размеров круговых индикаторов (в нашем проекте 4 размера, но для простоты кода мы оставим только 2). Стоит отметить, что Safari не поддерживает rem-единиц внутри svg-элементов. Однако он поддерживает em единиц. Поэтому мы устанавливаем font-size: 1rem для основного элемента, чтобы использовать внутри него em-юнитов.
@width: {
@m: 2em;
@l: 7em;
};
@track-stroke: {
@m: 0.5em;
@l: 0.25em;
};
@progress-stroke: {
@m: 0.5em;
@l: 0.375em;
};
Процесс заполнения прогресса происходит за счет установки stroke-dasharray и перерасчета stroke-dashoffset. Прочтите статью Быстро построить кольцо прогресса, чтобы понять, как это работает. Наше решение — это просто улучшенная версия того, что было предложено его автором. Создайте миксин, который вычисляет состояние индикатора прогресса для разных размеров компонентов:
.circle-params(@size) {
width: @width[ @@size];
height: @width[ @@size];
.track {
r: (@width[ @@size] - @track-stroke[ @@size]) / 2;
stroke-width: @track-stroke[ @@size];
}
.progress {
@radius: (@width[ @@size] - @progress-stroke[ @@size]) / 2;
@circumference: 2 * pi() * @radius;
r: @radius;
stroke-width: @progress-stroke[ @@size];
stroke-dasharray: @circumference;
stroke-dashoffset: calc(@circumference - var(--progress-percentage) * @circumference);
}
}
Наконец, примените наш миксин к элементу host и добавьте некоторые косметические улучшения:
:host {
display: block;
position: relative;
color: yellow;
transform: rotate(-90deg);
transform-origin: center;
font-size: 1rem;
&[data-size='m'] {
.circle-params(m);
}
&[data-size='l'] {
.circle-params(l);
}
}
.track {
fill: transparent;
stroke: grey;
}
.progress {
fill: transparent;
stroke: var(--tui-progress-color, currentColor);
transition: stroke-dashoffset 300 linear;
}
.hidden-progress {
.sr-only(); // see this mixin in the chapter with ProgressBar
}
.svg {
overflow: unset;
}
Вот и все! Вы можете увидеть компонент в действии на витрине нашего проекта Taiga UI. Окончательный исходный код доступен на GitHub.
💡 Совет. Теперь вы можете развернуть вновь созданные семантические компоненты на такой платформе, как Bit, чтобы их можно было повторно использовать во всех ваших проектах. С Bit у вас будет независимое управление версиями, тесты и документация для ваших компонентов, что упростит другим понимание и использование вашего кода. Это позволит вашей команде повторно использовать компоненты и совместно работать над ними для написания масштабируемого кода, ускорения разработки и поддержки согласованного пользовательского интерфейса. Узнайте больше здесь.
Подведение итогов
В фронтенд-разработке нет абсолютно правильных решений. Один и тот же функционал может быть реализован по-разному. Но есть поверхностные и ленивые решения, которые дают конечный продукт, жертвуя удобством и оптимизацией. Я показал, какие ошибки можно допустить, если вы хотите создать индикаторы прогресса: плохая семантика и пренебрежение доступностью.
В этой статье я показал свою точку зрения на эти компоненты. Я не утверждаю, что мои решения являются лучшими в этой области. И все же при их разработке я учел все упомянутые проблемы, обеспечил совместимость со всеми современными браузерами и получил простые решения с минимальным использованием JavaScript.
Создавайте приложения с повторно используемыми компонентами, как Lego

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.
Превратите любой пользовательский интерфейс, функцию или страницу в компонент многократного использования — и поделитесь им со своими приложениями. Легче сотрудничать и строить быстрее.
Разделите приложения на компоненты, чтобы упростить разработку приложений, и наслаждайтесь наилучшими возможностями для рабочих процессов, которые вы хотите: