Реализация SVG-анимаций (SMIL/CSS/JS) на сайте
SVG-анимации существуют в трёх форматах, каждый со своей нишей. SMIL (Synchronized Multimedia Integration Language) — декларативные анимации внутри SVG-разметки, работают без JavaScript. CSS-анимации через @keyframes — для простых трансформаций. JavaScript через Web Animations API или GSAP — для интерактивных и управляемых сцен. Рассмотрим все три подхода с реальными примерами.
SMIL: анимации внутри SVG
SMIL-анимации описываются прямо в SVG-коде через теги <animate>, <animateTransform>, <animateMotion>:
<!-- public/animations/logo.svg -->
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Анимация цвета заливки -->
<circle cx="100" cy="100" r="50" fill="#3b82f6">
<animate
attributeName="fill"
values="#3b82f6;#8b5cf6;#ec4899;#3b82f6"
dur="3s"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1; 0.4 0 0.2 1"
/>
<animate
attributeName="r"
values="50;45;50"
dur="1.5s"
repeatCount="indefinite"
/>
</circle>
<!-- Движение по пути -->
<circle r="8" fill="white">
<animateMotion
dur="4s"
repeatCount="indefinite"
rotate="auto"
>
<mpath href="#orbit-path" />
</animateMotion>
</circle>
<path
id="orbit-path"
d="M 100,30 A 70,70 0 1,1 99.9,30"
fill="none"
stroke="rgba(255,255,255,0.2)"
stroke-width="1"
/>
<!-- Морфинг формы через атрибут d -->
<path fill="#f59e0b">
<animate
attributeName="d"
dur="2s"
repeatCount="indefinite"
values="
M 100,20 L 180,80 L 150,160 L 50,160 L 20,80 Z;
M 100,10 L 190,90 L 160,170 L 40,170 L 10,90 Z;
M 100,20 L 180,80 L 150,160 L 50,160 L 20,80 Z
"
/>
</path>
</svg>
SMIL работает во всех современных браузерах кроме IE (уже не актуально). Нет поддержки в Safari iOS для некоторых атрибутов — нужно тестировать.
CSS-анимации SVG
CSS подходит для трансформаций, opacity, stroke-анимаций. Важный момент: transform-origin в SVG работает не так, как в HTML — координаты относятся к SVG viewport:
/* styles/svg-animations.css */
/* Пульсирующий индикатор */
.pulse-ring {
transform-origin: center;
animation: pulse 2s ease-out infinite;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
/* Отрисовка линии (stroke dasharray/dashoffset) */
.draw-path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease-in-out forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
/* Появление с задержкой для групп элементов */
.stagger-item {
opacity: 0;
transform: translateY(20px);
animation: fadeUp 0.5s ease-out forwards;
}
.stagger-item:nth-child(1) { animation-delay: 0.1s; }
.stagger-item:nth-child(2) { animation-delay: 0.2s; }
.stagger-item:nth-child(3) { animation-delay: 0.3s; }
@keyframes fadeUp {
to { opacity: 1; transform: translateY(0); }
}
// components/AnimatedLogo.tsx
export function AnimatedLogo() {
return (
<svg viewBox="0 0 100 100" className="w-16 h-16">
{/* Фоновое кольцо */}
<circle
cx="50" cy="50" r="40"
fill="none"
stroke="#e5e7eb"
strokeWidth="4"
/>
{/* Анимированный прогресс */}
<circle
cx="50" cy="50" r="40"
fill="none"
stroke="#3b82f6"
strokeWidth="4"
strokeLinecap="round"
className="draw-path"
style={{ transformOrigin: '50px 50px', transform: 'rotate(-90deg)' }}
/>
</svg>
)
}
JavaScript: Web Animations API
Web Animations API — нативный JS без библиотек, хорошая производительность:
// utils/svg-animator.ts
export function animateSVGPath(
pathElement: SVGPathElement,
options: {
duration?: number
easing?: string
delay?: number
} = {}
): Animation {
const length = pathElement.getTotalLength()
pathElement.style.strokeDasharray = `${length}`
pathElement.style.strokeDashoffset = `${length}`
return pathElement.animate(
[
{ strokeDashoffset: length },
{ strokeDashoffset: 0 },
],
{
duration: options.duration ?? 1500,
easing: options.easing ?? 'ease-in-out',
delay: options.delay ?? 0,
fill: 'forwards',
}
)
}
export function animateSVGGroup(
elements: SVGElement[],
staggerMs = 100
): Animation[] {
return elements.map((el, i) =>
el.animate(
[
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{
duration: 500,
delay: i * staggerMs,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
fill: 'forwards',
}
)
)
}
GSAP + SVG: продвинутый уровень
GSAP даёт максимальный контроль над SVG-анимациями, особенно для сложных временных линий:
// components/AnimatedDiagram.tsx
import { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin'
gsap.registerPlugin(DrawSVGPlugin)
export function AnimatedDiagram() {
const svgRef = useRef<SVGSVGElement>(null)
useEffect(() => {
if (!svgRef.current) return
const ctx = gsap.context(() => {
const paths = svgRef.current!.querySelectorAll('.data-path')
const nodes = svgRef.current!.querySelectorAll('.node')
const labels = svgRef.current!.querySelectorAll('.label')
const tl = gsap.timeline({ repeat: -1, repeatDelay: 2 })
// Отрисовка путей последовательно
tl.from(paths, {
drawSVG: '0%',
duration: 1.5,
stagger: 0.3,
ease: 'power2.inOut',
})
// Появление узлов
.from(nodes, {
scale: 0,
opacity: 0,
transformOrigin: 'center',
stagger: 0.15,
ease: 'back.out(2)',
duration: 0.5,
}, '-=0.5')
// Появление подписей
.from(labels, {
opacity: 0,
y: 10,
stagger: 0.1,
duration: 0.4,
}, '-=0.3')
}, svgRef)
return () => ctx.revert()
}, [])
return (
<svg ref={svgRef} viewBox="0 0 400 300">
<path className="data-path" d="M 50,150 C 150,50 250,50 350,150" stroke="#3b82f6" strokeWidth="2" fill="none" />
<circle className="node" cx="50" cy="150" r="8" fill="#3b82f6" />
<circle className="node" cx="200" cy="80" r="8" fill="#8b5cf6" />
<circle className="node" cx="350" cy="150" r="8" fill="#3b82f6" />
<text className="label" x="50" y="170" textAnchor="middle" fontSize="12">Начало</text>
<text className="label" x="350" y="170" textAnchor="middle" fontSize="12">Конец</text>
</svg>
)
}
Доступность SVG-анимаций
// Для декоративных анимаций
<svg aria-hidden="true" focusable="false">
{/* ... */}
</svg>
// Для информативных
<svg role="img" aria-label="Анимированный прогресс загрузки 75%">
<title>Загрузка файла</title>
<desc>Прогресс-бар показывает 75% завершения загрузки</desc>
{/* ... */}
</svg>
Respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
.draw-path,
.pulse-ring,
.stagger-item {
animation: none;
}
.draw-path { stroke-dashoffset: 0; }
}
Типичные сроки
Простые CSS stroke-анимации для иконок — 2–4 часа. SMIL-анимации для логотипа/иллюстрации — 1 день. Сложная JS-анимированная диаграмма/инфографика с GSAP — 2–4 дня.







