SVG Morphing Loaders
Three 54 px SVG indicators — blob (2.2 s), drop (1.6 s), star (1.9 s) — whose paths morph in a loop via native SMIL interpolation, with no JavaScript.
This effect is free — the Loaders category contains 29 effects total, including 4 free. Effect.Labs has 811 vanilla effects. Explore the category →
Usage examples
Processing
3 tasks in parallel · ~2 min leftOrbis
Workspace
Loading your workspace…
Application loadingHow it works
The effect relies on a .sml-row (flex, gap: 34px) that aligns three .sml-item elements (grid, place-items: center, gap: 8px). Each item contains two 54 × 54 px SVGs (viewBox="0 0 54 54") — .sml-anim for the animated version and .sml-static for the static fallback — plus an .sml-label (0.62 rem, uppercase, letter-spacing: 0.14em, rgb(143, 143, 176)). The Blob is filled by a linear gradient defined in an inline <linearGradient id="smlGradA"> (#7c3aed → #db2777, violet to rose); the Drop is #5a9bff (blue) and the Star is #ffd166 (yellow). The delivered code has no JavaScript: the switch between the two SVGs is pure CSS — .sml-item .sml-static { display: none } under normal rendering, plus a prefers-reduced-motion media query that inverts the two (detailed in the accessibility section).
Each animated SVG contains a single <animate attributeName="d"> that interpolates the <path>'s d attribute between three states (keyTimes="0;.5;1"), the first and last being identical — a symmetric loop. The Blob oscillates between a near-perfect oval and an asymmetric teardrop pointing upward; the Drop shifts between an upward-pointing teardrop and a disc; the five-branched Star tightens and shrinks inward. calcMode="spline" with keySplines=".4 0 .2 1;.4 0 .2 1" (cubic Bézier curve) eases in and out of each transition — the SMIL equivalent of ease-in-out. repeatCount="indefinite" keeps the loop running without interruption. Interpolation operates directly on the Bézier control-point coordinates of the path; the browser computes each frame.
The durations 2.2 s, 1.6 s and 1.9 s share no simple rational ratio: their least common multiple exceeds 50 seconds, making any resynchronisation imperceptible — the three shapes breathe independently, never in phase. The hiding rule is written .sml-item .sml-static { display: none } rather than .sml-static { display: none } for a CSS specificity reason: the stylesheet contains .sml-item svg { display: block } (one class plus one type selector, specificity 0-1-1), which applies to all six SVGs. A bare .sml-static (0-1-0) would lose that contest wherever it sits in the stylesheet — with an equal number of classes, the type selector tips the balance — and each item would then stack both of its SVGs (54 + 8 + 54 px) above the label. With two classes (0-2-0) the rule wins and each item renders a single SVG: 54 px, an 8 px gap, then the label.
Accessibility
- All three
.sml-animand.sml-staticcarryaria-hidden="true": no alternative text is announced. If the set represents a loading state, add a parent container withrole="status" aria-live="polite"and hidden text — for example<span class="sr-only">Loading…</span>. Without this, screen readers ignore the indicator entirely. - prefers-reduced-motion handled: the delivered CSS contains
@media (prefers-reduced-motion: reduce) { .sml-item .sml-anim { display: none } .sml-item .sml-static { display: block } }. When the preference is on, the animated SVG is no longer rendered and the static SVG — the resting shape, identical to the first state of each<animate>— takes its place: one SVG per indicator either way. A subtlety to preserve if you merge this CSS into a larger stylesheet:@mediaadds no specificity, and.sml-item .sml-static { display: block }has exactly the same specificity (0-2-0) as the hiding rule; source order decides, so the@mediablock must stay after it. - The labels are French in the delivered HTML (
Blob,Goutte,Étoile), and each<span class="sml-label">also carries adata-enattribute ("Blob", "Drop", "Star") that the English version of the site uses. In your project, edit the text directly to match your interface language —data-enis inert without a script that reads it, so you can remove it. If the labels are redundant with the surrounding page content, addaria-hidden="true"to each span. - Indicator contrast (WCAG 1.4.11, graphical objects, 3:1 threshold) on a
#0a0a0fbackground: Drop (#5a9bff) 7.1:1; Star (#ffd166) 13.7:1; Blob gradient from 3.5:1 (violet#7c3aed) to 4.3:1 (rose#db2777) — all three pass. On a light background the picture flips: the Star drops to 1.4:1 on white and the Drop to 2.8:1, both below the threshold; darken them (for example#b45309, 5.0:1, and#2563eb, 5.2:1) or keep a dark background. The Blob stays compliant (5.7:1 and 4.6:1).
Browser compatibility
Requires the SMIL <animate> element with calcMode="spline", SVG linearGradient and CSS display: grid. Zero dependencies, no JavaScript.
Without SMIL (Edge ≤ 18 / IE), the <animate> elements are ignored — and since the animated paths have no d attribute of their own (the animation supplies it), they draw nothing: the indicator leaves an empty 54 px box above its label, while the .sml-static SVGs stay hidden outside reduced-motion mode. Those browsers are no longer maintained; if you still have to cover them, copy onto each animated path the d attribute of its static twin: SMIL overrides it when available, and it stays visible otherwise.
The code
Copy the three blocks into your page. No dependencies.
<div class="sml-row">
<div class="sml-item">
<svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="url(#smlGradA)">
<animate attributeName="d" dur="2.2s" repeatCount="indefinite" calcMode="spline" keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1" values="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z;M27 8 C44 3 51 16 46 29 C42 44 30 52 18 46 C6 41 2 26 9 16 C14 8 20 10 27 8 Z;M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z">
</animate>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="url(#smlGradA)" d="M27 5 C41 5 49 13 49 27 C49 41 41 49 27 49 C13 49 5 41 5 27 C5 13 13 5 27 5 Z">
</path>
</svg>
<span class="sml-label" data-en="Blob">Blob</span>
</div>
<div class="sml-item">
<svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#5a9bff">
<animate attributeName="d" dur="1.6s" repeatCount="indefinite" calcMode="spline" keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1" values="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z;M27 10 C38 10 46 18 46 28 C46 39 38 46 27 46 C16 46 8 39 8 28 C8 18 16 10 27 10 Z;M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z">
</animate>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#5a9bff" d="M27 6 C34 16 44 24 44 33 C44 43 36 49 27 49 C18 49 10 43 10 33 C10 24 20 16 27 6 Z">
</path>
</svg>
<span class="sml-label" data-en="Drop">Goutte</span>
</div>
<div class="sml-item">
<svg class="sml-anim" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#ffd166">
<animate attributeName="d" dur="1.9s" repeatCount="indefinite" calcMode="spline" keyTimes="0;.5;1" keySplines=".4 0 .2 1;.4 0 .2 1" values="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z;M27 12 L31 22 L42 23 L34 30 L36 41 L27 36 L18 41 L20 30 L12 23 L23 22 Z;M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z">
</animate>
</path>
</svg>
<svg class="sml-static" width="54" height="54" viewBox="0 0 54 54" aria-hidden="true">
<path fill="#ffd166" d="M27 6 L33 20 L48 21 L37 31 L41 46 L27 38 L13 46 L17 31 L6 21 L21 20 Z">
</path>
</svg>
<span class="sml-label" data-en="Star">Étoile</span>
</div>
<svg width="0" height="0" aria-hidden="true">
<defs>
<linearGradient id="smlGradA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c3aed">
</stop>
<stop offset="1" stop-color="#db2777">
</stop>
</linearGradient>
</defs>
</svg>
</div>
.sml-row {
display: flex;
gap: 34px;
align-items: center;
justify-content: center;
}
.sml-item {
display: grid;
place-items: center;
gap: 8px;
}
.sml-item svg {
display: block;
}
.sml-label {
font-size: 0.62rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgb(143, 143, 176);
}
.sml-item .sml-static {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.sml-item .sml-anim {
display: none;
}
.sml-item .sml-static {
display: block;
}
}
// Effet CSS pur — pas de JavaScript nécessaire
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Animation durations (dur on each <animate>) |
2.2s (Blob) / 1.6s (Drop) / 1.9s (Star) | Lengthen all three for a slower, more contemplative morph; shorten for urgency. Avoid simple ratios (1:2, 2:3) to preserve the organic desynchronisation. |
Drop and Star colours (fill attribute on <path>) |
#5a9bff (Drop) / #ffd166 (Star) | Each path is independent. Use a solid hex, a gradient defined in <defs>, or currentColor to inherit the surrounding text colour. |
Blob colours (stops in <linearGradient id="smlGradA">) |
#7c3aed → #db2777 (violet to rose) | Edit the stop-color attributes on the two <stop> elements. For a flat Blob colour, replace fill="url(#smlGradA)" with a direct hex value. |
Indicator size (width/height attributes on each <svg>) |
54 px | The viewBox stays at 0 0 54 54 so the shape scales cleanly to any size. 32 px for a button, 72–96 px for a splash screen. No path edits needed. |
Transition curve (keySplines on each <animate>) |
.4 0 .2 1;.4 0 .2 1 | Two cubic values (one per segment 0→0.5 and 0.5→1). 0 0 1 1;0 0 1 1 = linear interpolation (mechanical); .8 0 .2 1;.8 0 .2 1 = very pronounced, long pause at each extreme. |
Gap between indicators (gap on .sml-row) |
34px | Compress to 16–20 px to fit a narrow button or toolbar; widen to 48–64 px for a panel or splash screen. |
Labels (content of <span class="sml-label">) |
Blob / Goutte / Étoile — data-en attribute: Blob / Drop / Star | Replace the text with action verbs ("Analyse", "Generate", "Check") or your interface language — the data-en attribute only serves the English version of Effect.Labs, so remove it or keep it in sync. To hide the labels: .sml-label { display: none }. |
FAQ
Why are the three durations 2.2 s, 1.6 s and 1.9 s never multiples of each other?
To prevent periodic resynchronisation. Two indicators whose durations share a simple rational ratio (e.g. 1.6 s and 3.2 s) visibly realign every 3.2 s; the repeat is noticeable. With 2.2 / 1.6 / 1.9 s, the first exact simultaneous realignment of all three requires more than 50 s — well beyond the viewer's attention span. The trio then appears to breathe freely. For a single indicator or identical durations, this reasoning does not apply.
Can I place two instances of this trio on the same page?
Not without modification. The id smlGradA must be unique in the document; the second instance's fill="url(#smlGradA)" resolves to the first definition — both Blobs share the same gradient, which renders but is fragile. For two independent instances, suffix the ids (smlGradA-1, smlGradA-2) and update the corresponding fill attributes.
How does this effect differ from Morphing Blob (fx-0506) and Morphing Shape (fx-0431)?
Technique and count. fx-0506 and fx-0431 animate the CSS border-radius property on a single <div> — CSS mechanics cannot produce a star or a teardrop, only rectangles with rounded corners. fx-0777 animates the SVG d attribute via SMIL <animate> on three distinct paths: the shapes (drop, star) can be arbitrary, and interpolation is handled by the SVG rendering engine without JavaScript. The desynchronised trio does not exist in either of the other effects.
Why does each indicator embed two SVGs? Can I keep just one?
The second SVG (.sml-static) is the motion-free fallback. A SMIL animation cannot be controlled from CSS: animation-play-state or animation: none have no effect on it, only svg.pauseAnimations() in JavaScript stops it. Without a script, the only way to honour prefers-reduced-motion is therefore to take the animated SVG out of the render (display: none) and show a frozen copy in its place — which is exactly what the delivered media query does. If you keep only the animated SVG, call pauseAnimations() on each <svg> at load time when matchMedia('(prefers-reduced-motion: reduce)').matches is true: the shape freezes on its first state — same result, at the cost of a few lines of JavaScript.