Search code examples
htmlcsssvgvitesvelte

How to dynamically color an imported SVG icon in Svelte without CSS filters?


I am running into trouble trying to achieve a simple task: with an icon imported into a Svelte component using Vite's asset import, how can I change the color of the icon?

<script>
  // This would be the usual Vite way to import the asset
  import icon from "../assets/icon.svg";
  // Our dynamic color
  export let iconColor = "#c0ffee";
</script>

From reading this question, several options seem to be available:

  • CSS filters, which I don't want to use, as they are too heavy performance-wise, both when it comes to rendering, and computing values for the filter (i.e. given a hex color, getting the filter values is a whole lot of JS).
  • Using CSS mask property. The mask needs to be a URL of an svg, so seems like it cannot work with an asset import.
  • A nice and straightforward approach: putting fill="currentColor" in the icon's source, and styling it using the CSS color property. This doesn't work, for a slightly complex reason:

Since the icon is an import, meant to be used with <img src={icon}/>, and not directly an <svg> element, the contents would need to inlined. This could be done like this:

<script>
  import icon from "../assets/icon.svg?raw";
</script>

<div> {@html icon} </div>

The resulting compiled page is indeed the SVG nested inside a div. But now there is no way to style it: adding an svg {} CSS selector would be considered "unused". Svelte has no way of knowing what's inside the raw HTML string we imported, so it tree shakes that selector away. Furthermore, due to this, we couldn't apply any other styling to the icon, so basic things like setting the icon size are not possible either.

Using :global on the style as suggested in this question doesn't help either: since a dynamic color per-icon is needed, each new instance of this component would recolor all icons in the page.

Trying to trick Svelte into not tree-shaking it fails: rules like div > * also get removed, and even adding an empty <svg style="display:none"> to the div doesn't work -- the style only gets applied to the hidden placeholder, but not the inlined SVG.

Maybe I'm overthinking this, and a simple solution exists that fullfils the criteria "have an SVG asset import", and "dynamically change its color and size". Would appreciate any help.


Solution

  • When you add fill="currentColor" to the svg and inline it via `@html' and a wrapper div, setting the color on the div would affect the svg

    <script>
        const svg = '<svg viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"></circle></svg>'
    
        export let color = 'teal'
    </script>
    
    <div style:color={color}>
        {@html svg}
    </div>
    

    Without adding fill="currentColor", using the wrapper div in combination with the :global modifier and a CSS variable would be an alternative way

    <script>
        const svg = '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"></circle></svg>'
    
        export let color = 'teal'
    </script>
    
    <div style:--color={color}>
        {@html svg}
    </div>
    
    <style>
        div :global(svg) {
            fill: var(--color);
        }
    </style>