Спиннер загрузки собирается без единой картинки: круг из border с одной стороной другого цвета плюс @keyframes, крутящий его через rotate(0deg) - rotate(360deg) по кругу. Важная деталь, которую легко упустить: timing-function обязательно linear, иначе вращение выглядит рывками. И раз спиннер - это просто крутящийся div, для незрячих пользователей он ничего не значит без role="status" и скрытого текста.
В этом уроке
Попробуй прямо сейчас: круг крутится ровно, без рывков - это заслуга animation-timing-function: linear. Поменяй linear на ease (или удали строку целиком - ease станет значением по умолчанию) и вращение станет неровным: то ускоряется, то замедляется, что для спиннера выглядит как техническая проблема, а не задуманный эффект. Обрати внимание и на текст "Загрузка..." внутри - глазами он не виден (спрятан классом .visually-hidden), но именно он объясняет экранному диктору, что вообще происходит на странице.
Спиннер из одной обводки: border-trick и rotate
Сам круг - это div с одинаковыми width и height, border-radius: 50% превращает квадрат в окружность. "Вращающийся" эффект даёт не движение, а обычная одноцветная обводка border, у которой ОДНА сторона (например border-top) перекрашена в другой, более яркий цвет через border-top-color или отдельное свойство border-top. Дальше @keyframes поворачивает весь элемент через transform: rotate() от 0deg до 360deg, и раз обводка неравномерная, вращение самого div создаёт иллюзию "бегущего" по кругу сегмента - хотя на самом деле крутится вся обводка целиком.
.spinner {
width: 48px;
height: 48px;
border: 5px solid #e5e7eb;
border-top: 5px solid #4f46e5;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
} - iteration-count: infinite здесь часть шорткада animation, крутится, пока страница открыта (уже разбирали в уроке про animation-duration и iteration-count).
.spinner {
border: 5px solid #e5e7eb;
border-top: 5px solid #4f46e5;
border-radius: 50%;
animation: spin 1s infinite;
} - timing-function не указан, по умолчанию ease: угол поворота растёт неравномерно, вращение выглядит рваным вместо ровного.
Спиннер и доступность: role="status" и prefers-reduced-motion
Для зрячего пользователя крутящийся кружок и так понятен без слов. Но для человека с экранным диктором пустой вращающийся div - это просто ничего, диктор его не озвучит и не расскажет, что страница загружается. Стандартное решение - role="status" на контейнере спиннера (у него уже встроено aria-live="polite" - объявление придёт, когда пользователь не занят чем-то другим, а не перебьёт его на середине действия) и текст внутри вроде "Загрузка...", спрятанный от глаз классом .visually-hidden, но доступный диктору.
<div class="spinner" role="status">
<span class="visually-hidden">Загрузка...</span>
</div> - роль status плюс скрытый текст: диктор скажет "Загрузка...", зрячий пользователь увидит только кружок.
<div class="spinner"></div> - без role и текста внутри диктор просто промолчит, будто на странице ничего не происходит.
Отдельный вопрос - нужно ли отключать вращение спиннера для пользователей, попросивших "меньше анимации" в настройках системы через @media (prefers-reduced-motion: reduce) - об этом упоминалось в прошлом уроке. Спиннер отличается от декоративных эффектов появления тем, что он функциональный - это единственный видимый сигнал "страница ещё не готова", поэтому многие сайты сознательно оставляют его вращаться даже при включённой настройке, отключая через reduced-motion только чисто декоративные анимации.
Частые ошибки новичков
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Не указывать animation-timing-function: linear для вращения спиннера | По умолчанию применяется ease - вращение выглядит рваным, то быстрее, то медленнее | Всегда явно указывать linear для бесконечного равномерного вращения |
| Оставлять спиннер пустым div-ом без role="status" и текста внутри | Пользователь с экранным диктором не узнаёт, что страница загружается | Добавлять role="status" и визуально скрытый текст вроде "Загрузка..." |
| Использовать role="alert" вместо role="status" для обычного индикатора загрузки | alert - для срочных прерывающих сообщений (ошибок), не для рутинной загрузки | Использовать role="status" - его "вежливое" aria-live="polite" подходит лучше |
| Полностью убирать анимацию спиннера через animation: none, если на неё завязан JS-код (например ждёт animationend) | Событие animationend перестаёт срабатывать, JS-логика ломается | Сокращать animation-duration до 1ms вместо полного отключения, если событие важно |
Шпаргалка
| Запись | Что означает |
|---|---|
| border + border-top другого цвета + border-radius:50% | Круг с "бегущим" сегментом без единой картинки |
| animation: spin 1s linear infinite; | Ровное бесконечное вращение - linear обязателен |
| animation-timing-function: ease (по умолчанию) | Вращение выглядит рваным - не годится для спиннера |
| role="status" + .visually-hidden текст | Экранный диктор объявит статус загрузки |
| role="alert" | Для срочных ошибок, не для обычной загрузки |
Коротко: отвечаем на вопросы
Почему у спиннера обязательно должен быть linear, а не ease?
При linear угол поворота растёт строго пропорционально времени - равные промежутки времени дают равные приросты угла, поэтому вращение выглядит ровным и предсказуемым. При ease (значение по умолчанию, если не указать timing-function явно) скорость вращения меняется на каждом обороте - на бесконечном вращении это не тонкая деталь, а сразу заметная рваная, дёрганая анимация вместо плавного кручения.
Нужно ли отключать вращение спиннера для пользователей с prefers-reduced-motion: reduce?
Это не жёсткое правило, а решение конкретного сайта. Спиннер отличается от декоративных эффектов появления тем, что он функциональный - это единственный видимый сигнал "страница ещё не готова", поэтому многие сайты сознательно оставляют его вращаться даже при включённой настройке "меньше анимации", отключая через reduced-motion только декоративные эффекты без функциональной нагрузки.