ICON OOP includes a small set of original animated SVG icons, drawn and animated for this site. Each is a single self-contained .svg file with the animation defined inside it. No JavaScript library, no build step, no runtime dependency.
They cover the cases where motion genuinely does work: a loading spinner, a ticking clock, a ringing bell, a drawing checkmark, a pulsing status dot.
The principle
Animate an icon when the motion communicates something: that work is happening, that something arrived, that a state changed. Do not animate an icon because the animation looks nice. A page where several icons move at once has no way to signal that something actually needs attention.
The three ways to animate an SVG
| Method | How it works | Verdict |
|---|---|---|
| CSS inside the SVG | A <style> block with keyframes, inside the file | The right default. Works standalone or inline |
| SMIL | <animate> and <animateTransform> elements | Good for path morphing, deprecated-then-unreally, but well supported |
| JavaScript | GSAP, anime.js, Web Animations API | Only when you need interaction or sequencing |
The icons on this site use CSS inside the file, which is why they animate identically whether you inline them, load them with <img>, or set them as a CSS background. That portability is the whole reason to choose CSS over JavaScript for simple icon motion.
The one thing CSS cannot do well is morph one shape into another, since interpolating between paths with different node counts is unreliable. That is SMIL's remaining strong case, along with animating along a motion path.
Where animation earns its place
- Loading and progress. The clearest case. A spinner tells the user the system is working and has not frozen. Anything over about a second of waiting needs one.
- Success confirmation. A checkmark that draws itself over roughly 400ms reads as an event, which is exactly what a confirmation is. A static tick that appears is easy to miss.
- Attention on arrival. A bell that rings once when a notification lands. Once. Not on a loop.
- Live status. A slowly pulsing dot for a live stream or an active connection.
- State transitions. Hamburger to close, play to pause, chevron rotating on expand. Motion here explains the relationship between the two states.
Where it does not
- Decorative motion in feature grids. Six icons pulsing on scroll is a distraction competing with your copy.
- Anything on an infinite loop that is not communicating ongoing activity. Loops draw the eye permanently, and the eye eventually resents it.
- Navigation icons. Users need to find these fast; motion slows recognition.
- Multiple simultaneous animations. Motion signals priority. Everything moving means nothing is prioritised.
- Long entrance animations. If the icon takes 800ms to appear, the user has read past it.
A useful test: turn the animation off and ask whether anything is lost. If the answer is only "it looks less lively", leave it off.
Respecting reduced motion
This is not optional. Vestibular disorders are common, and for affected users unnecessary motion causes real nausea and dizziness. Operating systems expose a reduced motion preference and CSS lets you read it.
@media (prefers-reduced-motion: reduce) {
.icon-animated * {
animation: none !important;
transition: none !important;
}
}
Two refinements worth making. First, put the media query inside the SVG file as well as in your stylesheet, so the icon behaves correctly even when loaded standalone through an <img> tag, where your page CSS cannot reach it. Second, do not simply kill everything: a spinner that stops spinning tells the user the app has frozen. For genuinely functional motion, reduce it rather than remove it, for example fade a spinner's opacity instead of rotating it.
Performance: which properties are cheap
Not all animated properties cost the same. Some can be handled by the GPU on their own layer, others force the browser to recalculate layout or repaint on every frame.
| Property | Cost |
|---|---|
transform, opacity | Cheap. Composited, no layout or paint |
stroke-dashoffset | Moderate. Repaints, but fine for one or two icons |
fill, stroke colour | Moderate. Repaint each frame |
r, cx, width, d | Expensive. Recalculates geometry every frame |
Prefer transform and opacity wherever the effect allows. The classic line-drawing effect uses stroke-dasharray and stroke-dashoffset, which is more expensive but perfectly acceptable for a single confirmation checkmark. It is not acceptable for forty icons animating on scroll.
Embedding matters more than you expect
| Method | Animation runs? | Page CSS can control it? |
|---|---|---|
Inline <svg> | Yes | Yes, fully |
<img src="icon.svg"> | Yes, if CSS or SMIL is inside the file | No |
CSS background-image | Yes, same condition | No |
<object> | Yes | No, but the SVG can run its own JS |
The key point: an SVG loaded through <img> is a separate document. Your page CSS cannot reach inside it, and neither can your reduced-motion rule. Self-contained files, with their own styles and their own media query, are the only ones that behave correctly in every embedding method.
Animating an icon yourself
The line-drawing effect is the most useful one to know, and it is about six lines.
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<style>
.draw { stroke-dasharray: 30; stroke-dashoffset: 30;
animation: draw .4s ease forwards; }
@keyframes draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
.draw { animation: none; stroke-dashoffset: 0; }
}
</style>
<path class="draw" d="M5 13l4 4L19 7"/>
</svg>
The idea: stroke-dasharray makes the line one long dash, stroke-dashoffset pushes that dash out of view, and animating the offset back to zero draws the line in. Set the dasharray to at least the path's total length. The full walkthrough, including getting path lengths and morphing, is in how to animate SVG icons.