2026年11月14日
2026年11月14日
How to Add SVG Animation in WordPress (CSS, SMIL and Lottie)
This article is a localized edition of the Japanese original. Read the Japanese original
SVG is a vector image format, so it stays sharp at any size, and you can animate it with CSS or with SVG's own features. You can build a spinning loading indicator, a checkmark that draws itself or a blinking icon without video or GIFs.
This article explains how to use SVG animation in WordPress themes and pages, starting with where to put the SVG. The code assumes you can edit your theme (or child theme).
Where you put the SVG changes what you can do
| How it is placed | CSS animation | SMIL | Control from the page's CSS and JavaScript |
|---|---|---|---|
Write <svg> directly in the HTML (inline SVG) |
Works | Works | Yes |
Load it with <img src="icon.svg"> |
Works if written in a <style> inside the SVG file |
Works | No |
Load it with CSS background-image |
Works if written in a <style> inside the SVG file |
Works | No |
An SVG loaded through <img> or as a background image is treated as a separate document from the page. Animations written inside the SVG file still run, but the page's CSS cannot change the color or motion of the shapes inside, and JavaScript cannot, for example, start the animation when you scroll to it. The examples in this article mostly use inline SVG because it is the easiest to control.
Method 1: Rotate an inline SVG with CSS
This example rotates the whole SVG, like a loading indicator. Write the following SVG in a theme template file (PHP) or in a "Custom HTML" block in the block editor.
<svg class="icon-spin" xmlns="http://www.w3.org/2000/svg"
width="40" height="40" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" fill="none"
stroke="#2271b1" stroke-width="2" stroke-dasharray="48 15"/>
</svg>
Put the CSS in your theme's (or child theme's) style.css or in "Additional CSS".
.icon-spin {
animation: icon-spin 1s linear infinite;
}
@keyframes icon-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Stop it for people who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.icon-spin {
animation: none;
}
}
The outer <svg> element behaves like an HTML element, so it rotates around its center. When you rotate a shape inside the SVG (<circle> or <path>), the default center of rotation is the top-left corner of the whole SVG. To rotate a shape around its own center, add the following to that shape.
.icon-spin circle {
transform-box: fill-box;
transform-origin: center;
}
When you write it in a Custom HTML block
Only users with the unfiltered_html capability (Administrators and Editors on a single site, Super Admins on multisite) can save inline SVG and <style> in the block editor. When an Author or Contributor saves it, the disallowed tags are stripped. An SVG you reuse often is easier to manage in a theme template or template part.
Method 2: Stroke animation that draws a line
This example makes a circle and a checkmark look as if they are being drawn. You hide the line by setting stroke-dasharray and stroke-dashoffset to the length of the line, then animate stroke-dashoffset to 0.
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg"
width="60" height="60" viewBox="0 0 52 52" aria-hidden="true">
<circle class="checkmark-circle" cx="26" cy="26" r="25"
fill="none" stroke="#00a32a" stroke-width="2"/>
<path class="checkmark-check" fill="none" stroke="#00a32a"
stroke-width="3" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
/* Circumference = 2 × 3.1416 × 25 ≈ 157.1, so use 158 */
.checkmark-circle {
stroke-dasharray: 158;
stroke-dashoffset: 158;
animation: checkmark-draw 0.6s ease-out forwards;
}
/* The check line is about 33.8 long, so use 34 */
.checkmark-check {
stroke-dasharray: 34;
stroke-dashoffset: 34;
animation: checkmark-draw 0.3s ease-out 0.6s forwards;
}
@keyframes checkmark-draw {
to { stroke-dashoffset: 0; }
}
/* When motion is stopped, show the finished drawing */
@media (prefers-reduced-motion: reduce) {
.checkmark-circle,
.checkmark-check {
animation: none;
stroke-dashoffset: 0;
}
}
For a shape whose line length you do not know, run document.querySelector('.checkmark-check').getTotalLength() in the browser developer tools console to get the length.
If you write only animation: none to stop the motion, the line stays hidden and nothing appears. Also set stroke-dashoffset: 0, as above.
Method 3: Use SMIL (SVG animation elements)
SVG also has its own animation mechanism (SMIL), where you put elements such as <animate> inside a shape. You do not need separate CSS, and it works inside an SVG file loaded with <img>.
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="20"
viewBox="0 0 60 20" role="img" aria-label="Loading">
<circle cx="10" cy="10" r="6" fill="#2271b1">
<animate attributeName="opacity" values="1;0.2;1" dur="1.2s" repeatCount="indefinite"/>
</circle>
<circle cx="30" cy="10" r="6" fill="#2271b1">
<animate attributeName="opacity" values="1;0.2;1" dur="1.2s" begin="0.2s" repeatCount="indefinite"/>
</circle>
<circle cx="50" cy="10" r="6" fill="#2271b1">
<animate attributeName="opacity" values="1;0.2;1" dur="1.2s" begin="0.4s" repeatCount="indefinite"/>
</circle>
</svg>
CSS prefers-reduced-motion does not stop SMIL animation. To respect the reduced motion setting, build the animation with CSS instead, or, for inline SVG, stop it from JavaScript by calling pauseAnimations() on the SVG element.
Method 4: Start the animation when it scrolls into view
This example starts the Method 2 checkmark when it enters the screen. Change the CSS so the animation runs only after JavaScript adds the is-visible class.
.animate-on-scroll .checkmark-circle,
.animate-on-scroll .checkmark-check {
animation-play-state: paused;
}
.animate-on-scroll.is-visible .checkmark-circle,
.animate-on-scroll.is-visible .checkmark-check {
animation-play-state: running;
}
Add a class to the SVG, such as class="checkmark animate-on-scroll", and load the following JavaScript (load it the same way as with wp_enqueue_script in Method 5).
// Add is-visible once 30% of the element is visible, then stop observing it
const targets = document.querySelectorAll('.animate-on-scroll');
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.3 });
targets.forEach((el) => observer.observe(el));
With this approach, if JavaScript does not run, the animation stays paused (and the line stays hidden). Do not use it for shapes that must be visible, or change it so that JavaScript adds the animate-on-scroll class.
Method 5: Display a Lottie animation
Lottie turns animations made in tools such as After Effects into JSON files and plays them in the browser as SVG. Playback uses a library called lottie-web.
Step 1: Put the JSON file in your theme
By default, the WordPress Media Library does not accept JSON uploads. Put the animation JSON file inside your child theme (for example animations/hero.json).
Step 2: Load lottie-web and an initialization script
Add the following to your child theme's functions.php. lottie-web 5.13.0 in the example is the version available on cdnjs as of October 2026.
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'lottie-web',
'https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.13.0/lottie.min.js',
array(),
'5.13.0',
array( 'in_footer' => true )
);
wp_enqueue_script(
'mytheme-lottie-init',
get_theme_file_uri( 'js/lottie-init.js' ),
array( 'lottie-web' ),
'1.0.0',
array( 'in_footer' => true )
);
} );
get_theme_file_uri() returns the child theme's URL if the file exists there, and the parent theme's URL otherwise. If you do not want to use an external CDN, put the lottie-web file in your theme and load it from there.
Create js/lottie-init.js in the child theme to load an animation for each element that has a data-lottie attribute.
// Play the JSON file named in the data-lottie attribute
document.addEventListener('DOMContentLoaded', () => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-lottie]').forEach((el) => {
lottie.loadAnimation({
container: el,
renderer: 'svg',
loop: el.dataset.lottieLoop !== 'false',
autoplay: !reduce, // Do not autoplay when reduced motion is preferred
path: el.dataset.lottie,
});
});
});
Step 3: Add a placeholder in the template
Add the element where the animation appears to your template file.
<div class="hero-animation"
data-lottie="<?php echo esc_url( get_theme_file_uri( 'animations/hero.json' ) ); ?>"
data-lottie-loop="true"
style="width: 300px; height: 300px;"
aria-hidden="true"></div>
If you host the JSON file on another domain (such as a CDN), that server must allow cross-origin requests. If you keep it on the same site, you do not need this.
If you want to upload SVG files to the Media Library
By default, WordPress does not allow SVG uploads. SVG can contain JavaScript, so if you allow it just by adding the extension with the upload_mimes filter, someone could upload an SVG that contains a malicious script.
If you allow SVG uploads, use a plugin that sanitizes the file on upload. For example, "Safe SVG", developed by 10up, sanitizes uploaded SVGs with the enshrined/svg-sanitize library and also has a setting to restrict which users can upload them.
JavaScript inside an SVG shown with <img> does not run. Even so, a script can run when someone opens the SVG file's URL directly, so do not skip sanitizing.
Accessibility notes
- Add
aria-hidden="true"to decorative SVGs so screen readers skip them. Give meaningful SVGsrole="img"and anaria-label. - For people who chose "Reduce motion" in their OS or browser, stop or tone down the motion with
prefers-reduced-motion: reduce. - The Web accessibility guidelines (WCAG 2.2 Success Criterion 2.2.2) require a way to stop motion that lasts more than 5 seconds. A loading indicator shown only while the page cannot be used is treated as an exception.
Summary
- To animate with the page's CSS or JavaScript, write the SVG inline. When you load it with
<img>, put the animation inside the SVG file - Use
@keyframesandanimationfor rotation and similar effects, andstroke-dasharraywithstroke-dashoffsetfor line drawing - SMIL runs even inside
<img>, butprefers-reduced-motiondoes not stop it - For Lottie, load lottie-web with
wp_enqueue_scriptand keep the JSON file in your theme - Allow SVG uploads only after installing a plugin that sanitizes them
Related articles
- How to add title and desc to SVG in WordPress for accessibility
- How to upload and use an SVG icon set in WordPress
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)