Анимация SVG
Линия, которая рисуется сама; галочка, которая появляется штрихом; подчёркивание, растущее из-под курсора. Всё двумя свойствами обводки.
Что ты научишься делать
- Поймёшь приём с dasharray и dashoffset — основу всех «рисующихся» линий
- Сделаешь анимированную галочку успеха и подчёркивание при наведении
- Выберешь между CSS, SMIL и скриптом осознанно
#Фокус, который объясняется за минуту
Эффект рисующейся линии выглядит сложным, а держится на двух свойствах пунктира.
stroke-dasharray задаёт длину штриха и промежутка. Если сделать штрих длиной во весь контур, пунктир превращается в сплошную линию — визуально ничего не меняется.
stroke-dashoffset сдвигает этот пунктир вдоль контура. Сдвинули на всю длину — линия ушла за пределы контура и исчезла. Анимируем сдвиг обратно к нулю — линия «рисуется».
#Откуда взять длину контура
Нужно точное число — длина линии в единицах viewBox. На глаз её не угадать: у кривой длина заметно больше, чем расстояние между концами. Есть три способа получить её честно.
| Способ | Как | Когда |
|---|---|---|
| Спросить браузер | path.getTotalLength() в консоли | Один раз при вёрстке — самый точный |
| Взять с запасом | Поставить заведомо большее число | Быстро, но старт анимации будет с задержкой |
| Задать самому | Атрибут pathLength="100" | Лучший вариант: длина становится ровно 100 |
#Подчёркивание, растущее при наведении
Тот же приём, но без SVG вовсе: линию рисует псевдоэлемент, а анимируется transform. Это дешевле для браузера, чем анимация ширины.
#Движение по траектории
Второй приём, который выглядит дорого, а стоит трёх строк: элемент едет вдоль произвольной кривой. Раньше это считали скриптом покадрово, теперь есть offset-path.
Путь задаётся той же строкой, что и в атрибуте d, а offset-distance говорит, насколько далеко по нему уехал элемент. Анимируется как раз расстояние.
.plane { offset-path: path("M0 60 C 60 0, 140 120, 220 20"); offset-distance: 0%; /* Разворачивает элемент по касательной к пути */ offset-rotate: auto; animation: fly 3s ease-in-out infinite alternate;}@keyframes fly { to { offset-distance: 100%; }}#CSS, SMIL или скрипт
У SVG есть собственный язык анимации — <animate>, он же SMIL. Плюс работают обычные CSS-анимации и, конечно, JavaScript. Выбор не про вкус.
| CSS | SMIL | JavaScript | |
|---|---|---|---|
| Что умеет | Всё, что можно задать свойством: обводку, transform, opacity, цвет | Ещё и форму пути, движение по траектории | Всё, включая реакцию на данные |
| Управление из кода | Через классы и переменные | Неудобное | Полное |
| Уважает reduced-motion | Да, через медиазапрос | Нет — надо отключать вручную | Надо проверять самому |
| Когда брать | В девяти случаях из десяти | Морфинг формы, движение по пути | Когда анимация зависит от данных или ввода |
#Как это выглядит в реальном проекте
Индикатор загрузки для кнопки. Ни одного изображения, ни одной библиотеки, семнадцать строк.
@layer components { .spinner { inline-size: 1.25em; block-size: 1.25em; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; } .spinner circle { /* Дуга в четверть окружности, которая едет по кругу */ stroke-dasharray: 25 75; transform-origin: center; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Без движения — просто статичное кольцо, а не пустота */ @media (prefers-reduced-motion: reduce) { .spinner circle { animation: none; stroke-dasharray: none; opacity: .45; } }}#Анимация по скроллу
Линию часто хотят рисовать по мере прокрутки. Самый надёжный способ сегодня — IntersectionObserver: блок появился в поле зрения, добавили класс, анимация пошла один раз.
Есть и чисто декларативный путь — animation-timeline: view(), привязка анимации к прокрутке без единой строки JavaScript. Он работает в браузерах на движке Chromium, но в остальных пока нет, поэтому его дают сверх рабочего варианта, а не вместо.
/* База: класс добавляет скрипт, работает везде */.line.is-visible { animation: draw .8s ease forwards; }/* Улучшение там, где поддерживается */@supports (animation-timeline: view()) { .line { animation: draw linear both; animation-timeline: view(); animation-range: entry 20% cover 60%; }}#Когда НЕ надо
- Анимировать
dбез крайней нужды. Морфинг формы — самая дорогая анимация из возможных: браузер пересчитывает геометрию на каждом кадре. Для двух-трёх состояний дешевле поменять весь SVG. - Бесконечная анимация вне экрана. Спиннер, который крутится в скрытом блоке, продолжает есть кадры. Останавливай через
IntersectionObserverили убирай элемент. - Анимация ради заполнения паузы. Если загрузка занимает 200 мс, спиннер успеет только мигнуть — это раздражает сильнее, чем ожидание. Показывай индикатор с задержкой в 300–400 мс.
- Забыть про
prefers-reduced-motion. Для части людей движение на экране вызывает физическое недомогание. Это не настройка вкуса.
#Поддержка браузерами
| Возможность | Состояние |
|---|---|
Анимация stroke-dashoffset через CSS | Везде и давно |
pathLength | Во всех современных браузерах |
SMIL (<animate>) | Работает везде, но развивается слабо |
animation-timeline: view() | Пока только в браузерах на Chromium — давать сверх рабочего варианта |
offset-path (движение по траектории) | Во всех современных браузерах |
Где спотыкаются почти все
Длина контура подобрана на глаз
Почему Если число больше реальной длины, линия сначала «висит» невидимой, и анимация начинается с задержкой. Если меньше — появляется разрыв.
Как надо Ставь pathLength="100" и работай в процентах.
Анимируется fill или ширина вместо transform
Почему Такие свойства заставляют браузер пересчитывать раскладку на каждом кадре. На слабом устройстве анимация начинает дёргаться.
Как надо По возможности анимируй transform и opacity. Обводка — исключение: она дешёвая.
Забыт transform-origin у вращения в SVG
Почему По умолчанию точка отсчёта — левый верхний угол системы координат, и фигура улетает по дуге вместо вращения на месте.
Как надо Пиши transform-origin: center и transform-box: fill-box.
Нет ветки prefers-reduced-motion
Почему Человеку с вестибулярными нарушениями движущаяся графика может испортить самочувствие.
Как надо Добавь медиазапрос: анимацию отключи, а конечное состояние оставь видимым.
- Пунктир обводки stroke-dasharray
- Длины штриха и промежутка. Штрих во всю длину контура выглядит сплошной линией.
- Сдвиг пунктира stroke-dashoffset
- Смещение пунктира вдоль контура. Анимация этого свойства даёт эффект рисования.
- Заданная длина pathLength
- Атрибут, переопределяющий длину контура для расчётов обводки.
- SMIL SMIL
- Собственный язык анимации SVG через тег
animate.
Проверь себя
Штрих длиной во весь контур сдвинулся ровно на эту длину — видимой части не осталось.
Реальная геометрия не меняется, меняется система отсчёта для обводки. Считать длину больше не нужно.
CSS покрывает почти всё. У SMIL остаются две вещи, которых в CSS нет: анимация самого <code>d</code> и движение по пути.
По умолчанию точка отсчёта в SVG — начало системы координат, а не центр фигуры.
Для части людей движение вызывает физическое недомогание. Анимацию отключают, а конечное состояние оставляют видимым.
Задание
Оживить галочку
Галочка нарисована, но появляется сразу. Сделай так, чтобы контур рисовался: задай обоим элементам pathLength="100", спрячь их через stroke-dasharray и stroke-dashoffset, опиши @keyframes и запусти анимацию — галочку с задержкой после кольца.
Обязательно добавь ветку prefers-reduced-motion: без движения галочка должна быть просто видна.
Подсказка 1
Атрибут пишется прямо в разметке: <circle class="ring" ... pathLength="100"/>
Подсказка 2
Прячем линию: .ring { stroke-dasharray: 100; stroke-dashoffset: 100; }
Подсказка 3
Кадры и запуск: @keyframes draw { to { stroke-dashoffset: 0; } } и animation: draw .5s ease forwards;
Подсказка 4
Задержка — третье значение в сокращении: animation: draw .35s ease .45s forwards;
Показать решение
<svg viewBox="0 0 52 52" width="90" height="90" role="img" aria-label="Готово"> <circle class="ring" cx="26" cy="26" r="23" pathLength="100"/> <path class="tick" d="M15 27l8 8 15-16" pathLength="100"/></svg>svg { fill: none; stroke-linecap: round; stroke-linejoin: round; }.ring { stroke: #0f8f6a; stroke-width: 3; stroke-dasharray: 100; stroke-dashoffset: 100; animation: draw .5s ease forwards;}.tick { stroke: #0f8f6a; stroke-width: 4; stroke-dasharray: 100; stroke-dashoffset: 100; animation: draw .35s ease .45s forwards;}@keyframes draw { to { stroke-dashoffset: 0; }}@media (prefers-reduced-motion: reduce) { .ring, .tick { animation: none; stroke-dashoffset: 0; }}