Styling SVG with CSS

Presentation attributes, the cascade, currentColor, and custom properties

Presentation attributes vs CSS

SVG defines two ways to set visual properties on an element: presentation attributes (like fill="red" directly on the element) and CSS (like .my-circle { fill: red; } in a stylesheet or style="fill:red" inline). Both set the same underlying visual property, but they sit at different positions in the CSS cascade.

The precedence order from lowest to highest priority is:

  1. Presentation attributes (fill="..." on the element) — treated like a very-low-specificity author rule
  2. CSS class or type rules (.shape { fill: ... }) — override the attribute even at zero specificity
  3. Inline style attribute (style="fill:...") — overrides both
  4. !important declarations — override everything

This matters in practice: you can ship SVG files with presentation attributes set by an illustration tool, then override specific colors entirely in CSS without touching the SVG markup. The demo below proves it — the shape has fill="#d6452c" as an attribute, but the CSS class rule wins.

Attribute vs CSS rule

The rectangle below carries fill="#d6452c" as a presentation attribute, but the page's <style> block contains .override-demo .overridden { fill: #1f8a4c; }. The CSS class wins because any CSS rule — even a simple class rule — outranks a presentation attribute.

/* This CSS rule overrides the presentation attribute fill="#d6452c" */ .override-demo .overridden { fill: #1f8a4c; } <svg class="override-demo" viewBox="0 0 200 80" width="300" height="120" role="img" aria-label="Rectangle showing CSS fill overriding a presentation attribute"> <!-- fill="#d6452c" is the presentation attribute; the .overridden CSS class overrides it to green --> <rect class="overridden" x="10" y="15" width="80" height="50" fill="#d6452c" /> <!-- No class; presentation attribute is respected --> <rect x="110" y="15" width="80" height="50" fill="#d6452c" /> <text x="50" y="78" text-anchor="middle" font-size="9" fill="#333">CSS wins (green)</text> <text x="150" y="78" text-anchor="middle" font-size="9" fill="#333">attribute (red)</text> </svg> CSS wins (green) attribute (red)

The left rectangle has the overridden class and renders green despite fill="#d6452c" being right there on the element. The right rectangle has no class, so the attribute controls its color. This is exactly how design systems override icon colors in third-party SVG assets without forking the files.

Inline style takes priority over CSS rules

Adding style="fill:#e07b00" directly to an element bumps it above any stylesheet rule. You can override that in turn only with !important — the same cascade rules as ordinary HTML styling.

<svg viewBox="0 0 200 80" width="300" height="90" role="img" aria-label="Cascade specificity comparison: three rectangles"> <!-- Presentation attribute only --> <rect x="5" y="15" width="55" height="40" fill="#d6452c" /> <text x="32" y="65" text-anchor="middle" font-size="8" fill="#555">attr</text> <!-- CSS class overrides the attribute --> <rect class="overridden" x="72" y="15" width="55" height="40" fill="#d6452c" /> <text x="99" y="65" text-anchor="middle" font-size="8" fill="#555">class</text> <!-- Inline style overrides the CSS class --> <rect class="overridden" x="140" y="15" width="55" height="40" fill="#d6452c" style="fill:#e07b00" /> <text x="167" y="65" text-anchor="middle" font-size="8" fill="#555">inline style</text> </svg> attr class inline style

currentColor

currentColor is a special CSS keyword that resolves to whatever color is in effect on the element or its nearest ancestor. In SVG, you can use it as the value for fill, stroke, or any other color-accepting property. This makes SVG icons automatically inherit the text color of their surrounding HTML — a powerful technique for building icon systems.

The mechanics: when the browser sees fill="currentColor" (or fill: currentColor in CSS), it walks up the DOM tree looking for the computed value of the color property, then applies that as the fill. Set color on a parent element and every currentColor SVG inside it recolors automatically.

Icon that follows its parent's color

The three icon copies below are identical SVG markup. Only the color CSS property on the wrapping container differs. Because the paths use fill="currentColor", each icon picks up a completely different shade without any SVG-specific CSS.

.color-host-red { color: #d6452c; } .color-host-teal { color: #0f766e; } .color-host-amber { color: #e07b00; } <!-- Same SVG markup in three different color containers --> <figure class="svg-grid"> <figure class="color-host-red"> <svg viewBox="0 0 60 60" width="80" height="80" role="img" aria-label="Star icon, red"> <polygon points="30,4 36,20 54,20 40,31 46,48 30,37 14,48 20,31 6,20 24,20" fill="currentColor" /> </svg> <figcaption>color: #d6452c</figcaption> </figure> <figure class="color-host-teal"> <svg viewBox="0 0 60 60" width="80" height="80" role="img" aria-label="Star icon, teal"> <polygon points="30,4 36,20 54,20 40,31 46,48 30,37 14,48 20,31 6,20 24,20" fill="currentColor" /> </svg> <figcaption>color: #0f766e</figcaption> </figure> <figure class="color-host-amber"> <svg viewBox="0 0 60 60" width="80" height="80" role="img" aria-label="Star icon, amber"> <polygon points="30,4 36,20 54,20 40,31 46,48 30,37 14,48 20,31 6,20 24,20" fill="currentColor" /> </svg> <figcaption>color: #e07b00</figcaption> </figure> </figure>
color: #d6452c
color: #0f766e
color: #e07b00

This technique is the foundation of most CSS icon systems. You ship one SVG (or one <symbol>), toggle a single CSS property, and all icon instances in that context recolor together. It also integrates with dark mode: when your theme switches the base color property on body, icons using currentColor adapt automatically.

CSS custom properties in SVG

CSS custom properties (also called CSS variables) cascade into SVG just like any other inherited CSS value. You can declare --my-color: red on any ancestor element and reference it with var(--my-color, fallback) inside SVG properties. This unlocks real theming: swap a handful of custom properties on a container, and every icon or graphic inside it re-renders accordingly.

The syntax is identical to HTML theming: define the property on a parent and consume it in the SVG child. The fallback value in var(--icon-fill, #555) ensures the SVG renders even if the property is not set — useful for standalone SVG files dropped into different contexts.

Themed icon set via custom properties

.theme-a { --icon-fill: #d6452c; --icon-stroke: #333; } .theme-b { --icon-fill: #1f8a4c; --icon-stroke: #555; } .theme-c { --icon-fill: #e07b00; --icon-stroke: #333; } .themed-icon { fill: var(--icon-fill, #555); stroke: var(--icon-stroke, #333); stroke-width: 1.5; } <figure class="svg-grid"> <figure class="theme-a"> <svg viewBox="0 0 60 60" width="90" height="90" role="img" aria-label="House icon, red theme"> <polygon class="themed-icon" points="30,6 54,28 50,28 50,54 36,54 36,38 24,38 24,54 10,54 10,28 6,28" /> </svg> <figcaption>.theme-a</figcaption> </figure> <figure class="theme-b"> <svg viewBox="0 0 60 60" width="90" height="90" role="img" aria-label="House icon, green theme"> <polygon class="themed-icon" points="30,6 54,28 50,28 50,54 36,54 36,38 24,38 24,54 10,54 10,28 6,28" /> </svg> <figcaption>.theme-b</figcaption> </figure> <figure class="theme-c"> <svg viewBox="0 0 60 60" width="90" height="90" role="img" aria-label="House icon, amber theme"> <polygon class="themed-icon" points="30,6 54,28 50,28 50,54 36,54 36,38 24,38 24,54 10,54 10,28 6,28" /> </svg> <figcaption>.theme-c</figcaption> </figure> </figure>
.theme-a
.theme-b
.theme-c

The SVG markup is identical across all three — only the parent class differs. To add a fourth theme you add one CSS rule with two custom property declarations; no SVG markup changes at all. This separation of concerns scales well: a design token system (e.g. a dark mode toggle) can redefine --icon-fill globally and every icon on the page responds.

Targeting parts & interaction

When SVG is inlined directly in an HTML document, its elements participate fully in the CSS cascade just like any other DOM node. You can add class and id attributes to individual SVG child elements — <rect>, <circle>, <path>, etc. — and target them from your page stylesheet.

This also unlocks CSS pseudo-classes like :hover, :focus, and :active on SVG shapes. The browser fires pointer events on the visible fill area of each shape, so a rect:hover rule triggers when the mouse enters that rectangle's painted region. This does not work for SVG loaded via <img src="..."> — those are treated as opaque images with no accessible DOM.

Hoverable shape (inline SVG only)

The hexagon below changes from green to red and scales up slightly on hover. Open DevTools and inspect it — it is a real <polygon> in the DOM, styled exactly like any other element.

.hoverable-shape { cursor: pointer; fill: #1f8a4c; transition: fill 0.2s ease, transform 0.2s ease; transform-origin: 50px 50px; transform-box: fill-box; } .hoverable-shape:hover { fill: #d6452c; transform: scale(1.15); } <svg viewBox="0 0 100 110" width="140" height="154" role="img" aria-label="Interactive hexagon that changes color on hover"> <polygon class="hoverable-shape" points="50,5 93,27.5 93,72.5 50,95 7,72.5 7,27.5" /> <text class="hoverable-label" x="50" y="105">hover me</text> </svg> hover me

The transform-box: fill-box declaration is important: by default, CSS transforms on SVG elements use the SVG viewport as the reference box, which causes unexpected pivot points. Setting transform-box: fill-box makes the element transform relative to its own bounding box — so transform-origin: 50px 50px (or simply 50% 50%) centers the scale correctly on the shape itself.

For more complex interactive diagrams — tooltips, selection states, click handlers — inline SVG with CSS classes and :hover/:focus is the right starting point. For programmatic animation and event-driven behavior, the next step is the JavaScript DOM API, which treats SVG elements as first-class nodes.