SVG Styling & Animation
|
This section documents general HTML5 and CSS concepts — it is not tied to any specific framework or library. This content was generated with the assistance of AI. Verify it against current MDN documentation and browser-support tables (caniuse.com) before relying on it in production, since HTML/CSS features and browser support continue to evolve. |
Unlike the rest of this section, this topic is not covered by the reference book used elsewhere here — it is written entirely from general web-development knowledge. This page covers how Scalable Vector Graphics (SVG) interacts with CSS: the difference between inline SVG and SVG referenced as an external image, styling SVG shapes with presentation properties and CSS, animating SVG with CSS transitions/animations, and a brief note on the legacy SMIL animation syntax.
Inline SVG vs. <img src="*.svg">
SVG can be included in an HTML page in several ways, but only two are common in practice: as an external image reference, or written directly (inline) into the HTML document.
<!-- External reference: the browser treats the SVG file like any other raster image -->
<img src="icon.svg" alt="Star icon">
<!-- Inline SVG: the shapes become part of the page's own DOM -->
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M12 2 L15 9 L22 9.5 L17 14.5 L18.5 21 L12 17.5 L5.5 21 L7 14.5 L2 9.5 L9 9 Z"
fill="gold" stroke="orange" stroke-width="1"/>
</svg>
The distinction matters because of what each approach allows:
| Capability | <img src="*.svg"> |
Inline <svg> |
|---|---|---|
Styled with external/embedded CSS |
No |
Yes |
Styled with inline |
No (only the file’s own internal styles apply) |
Yes |
Manipulated/animated with JavaScript |
No |
Yes |
Individual sub-elements (paths, groups) selectable |
No — the whole image is one opaque unit |
Yes |
Cached as a separate HTTP request |
Yes |
No (inlined into the page’s own markup) |
CSS custom properties (variables) can cross into the SVG |
No |
Yes |
An <img>-referenced SVG is rendered as a self-contained, opaque image: the browser does not expose its
internal elements to the surrounding page’s CSS or JavaScript, even though the SVG file itself can contain its
own <style> block and even its own SMIL animations (see below). If a shape, path, or group needs to be
targeted, styled, or animated from the containing page’s stylesheet or scripts, the SVG must be inlined
directly into the HTML (or injected into the DOM via fetch + innerHTML, or <object>/<iframe> embedding
with same-origin access, which behave similarly to inlining for styling purposes).
Styling SVG with presentation properties and CSS
SVG shapes expose their own visual properties, most importantly fill (the interior color), stroke (the
outline color), stroke-width (the outline thickness), and stroke-dasharray (which turns a solid outline
into a dashed one, and is also the basis of the line-draw animation technique shown further below).
These can be set directly as XML attributes on the element:
<circle cx="12" cy="12" r="10" fill="none" stroke="#3b82f6" stroke-width="2" stroke-dasharray="4 2"/>
…but for anything beyond a static one-off shape, it is generally preferable to set them via CSS instead,
because CSS properties can use selectors, cascade, respond to state (:hover, :focus), and participate in
transitions/animations, none of which plain XML attributes support on their own:
circle {
fill: none;
stroke: #3b82f6;
stroke-width: 2;
stroke-dasharray: 4 2;
}
circle:hover {
stroke: #ef4444;
}
A few things to keep in mind when styling SVG with CSS:
-
For inline SVG, ordinary CSS selectors work on SVG elements exactly as they would on HTML elements — by tag name (
path,circle,rect),class,id, descendant/child combinators,:hover,:nth-child(), and so on. -
Most SVG presentation attributes (
fill,stroke,stroke-width,stroke-dasharray,opacity, and others) have a matching CSS property of the same name, and the CSS property always wins over the XML attribute when both are present — the normal CSS cascade and specificity rules apply, with the XML attribute acting as if it were the lowest-priority (user-agent-adjacent) style. -
Some properties are SVG-specific and have no HTML equivalent, such as
stroke-dasharray,stroke-dashoffset,stroke-linecap, andfill-rule— these only make sense on SVG shapes and are simply not recognized on regular HTML elements. -
currentColorand CSS custom properties (var(--icon-color)) both work insidefill/stroke, which is a common technique for building a single icon that recolors itself based on inherited/contextual CSS, without needing multiple copies of the SVG file.
Animating SVG with CSS transitions and animations
Once an SVG is inline, its shapes can be transitioned or keyframe-animated exactly like any other CSS-styled
element — including properties that are SVG-only, such as stroke-dashoffset, and geometric properties like
cx/cy/r (which, in browsers that support "SVG geometry properties as animatable CSS properties" — all
current evergreen browsers — can also be transitioned directly).
/* Simple hover transition on a presentation property */
.icon-heart path {
fill: none;
stroke: currentColor;
transition: fill 0.2s ease-in-out;
}
.icon-heart:hover path {
fill: currentColor;
}
/* Keyframe animation rotating a whole group indefinitely */
.spinner {
transform-origin: 50% 50%;
animation: spin 1.2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
transform on SVG elements works the same way it does on HTML elements once transform-box is considered:
by default, SVG elements use their own local coordinate system for transform-origin (0 0 being the SVG
viewport’s origin, not the shape’s own bounding box), so transform-origin: center may not center on the shape
itself unless transform-box: fill-box is also set. This is one of the more common surprises when animating
SVG rotation/scale with CSS.
Worked example: an animated "draw-in" line icon
A frequently used effect is making an outlined icon look like it is being hand-drawn, by animating
stroke-dashoffset. The technique: set stroke-dasharray to the path’s total length (so there is exactly one
dash covering the whole line, followed by one equally long gap), then animate stroke-dashoffset from that
same length down to 0, which slides the visible dash into place along the path.
<svg class="icon-check" viewBox="0 0 52 52" width="52" height="52">
<circle class="icon-check__circle" cx="26" cy="26" r="24" fill="none"/>
<path class="icon-check__mark" fill="none" d="M14 27 L22 35 L38 17"/>
</svg>
.icon-check__circle,
.icon-check__mark {
stroke: #22c55e;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Path length is known in advance (measured once, e.g. via element.getTotalLength()
in a browser console, or supplied directly with the pathLength attribute) */
.icon-check__circle {
stroke-dasharray: 151; /* approximate circumference of the r=24 circle */
stroke-dashoffset: 151;
animation: draw 0.6s ease-out forwards;
}
.icon-check__mark {
stroke-dasharray: 30; /* approximate length of the check-mark path */
stroke-dashoffset: 30;
animation: draw 0.3s ease-out 0.6s forwards; /* starts after the circle finishes */
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
The pathLength attribute (supported on <path>, <circle>, and other SVG shapes) can also be set to a
convenient round number, such as pathLength="100", so that stroke-dasharray/stroke-dashoffset can always
be expressed as a percentage of total length (100 and 0) regardless of the shape’s actual geometry — avoiding the need to measure the real path length at all.
SMIL: the legacy alternative
Before CSS animations/transitions were reliable across browsers, SVG had (and still has) its own native
animation syntax defined by SMIL (Synchronized Multimedia Integration Language), expressed as <animate>,
<animateTransform>, and <animateMotion> elements nested directly inside the SVG markup:
<circle cx="26" cy="26" r="24" fill="none" stroke="#22c55e" stroke-width="3">
<animate attributeName="stroke-dashoffset" from="151" to="0" dur="0.6s" fill="freeze"/>
</circle>
SMIL animations are self-contained within the SVG file itself, which is precisely why they still work even
when the SVG is loaded via <img src="*.svg"> — unlike CSS animations, which require the SVG to be inlined
into the page (or otherwise DOM-accessible) to take effect. That portability was SMIL’s main advantage.
In modern development, however, CSS transitions/animations (or JavaScript-driven animation) are generally
preferred over SMIL: CSS animations are easier to author and maintain alongside the rest of a page’s styling,
integrate with the same tooling (preprocessors, CSS-in-JS, developer-tools inspectors) already used for HTML,
and Chromium briefly deprecated SMIL support in the past before reversing course, which left lingering
uncertainty about its long-term status. SMIL remains functional in current browsers and is still a reasonable
choice for a self-contained animated icon file that must animate even when only referenced via <img>, but
CSS-based animation is the more broadly recommended default today.