How do you integrate inline SVGs?

We use lots of SVGs for icons in our application. They have to be inline svgs so we can style them using CSS.

e.g. <div><svg>...</svg></div>

SVG as image source cannot be styled e.g. <img src="logo.svg" />

Our current process is to get an SVG from our designer and convert it to Elm code using https://mbylstra.github.io/html-to-elm/ but I’m not happy with this because:

  • There is this extra step of converting and cleaning the SVGs
  • We cannot give this converted svgs back to our designer for modification, we need to keep a parallel library of icons in SVG format

So I’m wondering if anyone knows of a better process for working with SVGs in Elm, anything that we don’t know about?

2 Likes

I love using SVGs too. Converting to Elm is great if you have to do dynamic things to them, but you can just use Svg use.

I’ve made an Ellie that demonstrates here.

1 Like

I think @luke has a great method for this in Ellie, check out https://github.com/lukewestby/ellie/blob/master/client/src/Ellie/Ui/Icon.elm

There is a union type of all of the available icons, and then they are injected into the page https://github.com/lukewestby/ellie/blob/master/client/src/Ellie/Ui/Icon/Loader.js

In the view you end up with something like div [ Styles.infoIcon ] [ Icon.view Icon.Tag ]

2 Likes

Using xlinkHref looks like a really nice solution, thanks. will try this out.

1 Like

I concur.

I use the same method as @luke to display them (Svg.xlinkHref). The slight difference is that I build an svg sprite using svg-sprite-loader that is chained with svgo-loader to optimize it (for Ellie, svg-inline-loader is used).

My webpack3 config looks like:

      {
        test: /\.svg$/,
        use: [
          {
            loader: 'svg-sprite-loader',
            options: {}
          },
          'svgo-loader'
        ]
      }

I just added this in my app.js to generate and load the sprite:

// Build SVG sprite from icons
var req = require.context('./icons', true, /\.svg$/);
req.keys().forEach(req);

So to add an icon, I just copy it to the /icons directory, add a value to my Icon union type and a case branch to my Icon.view function, and that’s it (I could even use toString on the union type but I’m not sure how this will work in 0.19). It works great.

Edit: the main drawbacks are that the full sprite is always inserted in the page uncached, that unused icons are not removed, and that styling/animating is more limited than with inline SVGs, see the discussion below.

2 Likes

I remember there were some issues with styling SVGs that are embedded with the use element. For example if you have the following in your document:

<svg>
  <use xlink:href="#my-icon"></use>
<svg>

you can’t easily target an individual path inside that icon with CSS. There are some workarounds like using CSS variables, but I find it easier to just use inline SVG. I’m personally okay with converting icons to Elm by hand every time they are changed. In my experience that doesn’t happen very often.

At Culture Amp we use a custom Webpack loader based on NoRedInk/elm-assets-loader, which we’d be happy to open-source if it would be useful (or I can just post it in a Gist – there’s not a lot to it) and svg-sprite-loader.

The result is that any SVGs that are referenced in our Elm app are pulled into our Webpack build. At runtime, JavaScript injects all of the icons into the document as a hidden collection of SVG symbols at the start of the document body. The SVGs in our views are rendered as <svg><use xlink:href="#unique-symbol-id"/></svg>, which can be styled with CSS. It works great!

Yes, the ability to style icons referenced in this way is somewhat limited. You can style the svg element with two properties: color and fill. The value of color can then be used inside the SVG as currentColor (we have our SVG optimiser – another webpack loader – configured to replace solid black with currentColor).

The value of fill is inherited by all descendant SVG elements and therefore can technically be used to skin the SVG too; however, our SVG authoring tool (Sketch) exports SVGs with a wrapper element, <g fill="none">, that prevents this fill property from propagating usefully into nested elements, so right now we’re unable to use this technique to style a second colour; however, we’ve found that a single stylable colour is all we need.

What are the benefits of using SVG sprites over inline SVG? Sprites are definitely useful for server-side rendered apps where inline SVG can increase the size of HTML documents, but it seems to me that if you’re rendering on the client, inline SVG is the most powerful (and also the easiest) way to manage icons.

SVG sprites can still benefit from browser caching if they are used with an external reference (which is not what I do in my previous example), however IE browsers do not support this natively, so this requires using SVG For Everybody which is not perfect or some AJAX trickery.

Inline SVGs are clearly more powerful for styling and almost mandatory for animating them.

So overall I would say you’re right, but once you have a webpack config in place, I find it easier to add or update icons than with inline SVGs. Or is there a way to inject them automatically inline during build in elm generated code (including an svgo processing pass)?

Edit: it would certainly be possible to do this by modifying elm-assets-loader (most likely close to what @kevinyank described, but injecting directly the SVG after some processing instead of generating a sprite, but I don’t know a tool that already does it.)

Would better tooling solve this? A Sketch plug-in to output elm svg for instance?

Thinking more about it, elm-assets-loader should be able to load the file and inject it in the elm generated code using svg-inline-loader. I have never tried because I use webpack3 and it is not yet compatible with it.

But I gave it a try using a webpack2 elm test project and it works, and chaining through svgo-loader works also.

Here is my webpack2 config part:

            rules: [{
                test: /\.elm$/,
                exclude: [/elm-stuff/, /node_modules/],
                use: [
                    {
                        loader: 'elm-assets-loader',
                        options: {
                            module: 'My.Assets',
                            tagger: 'AssetPath',
                            package: 'test/test'
                        }
                    },
                    'elm-webpack-loader'
                ]
            },
            {
                test: /\.svg$/i,
                use: [{
                    loader: 'svg-inline-loader',
                },
                {
                    loader: 'svgo-loader',
                    options: {
                        plugins: [
                            { removeTitle: true },
                            { convertColors: { shorthex: false } },
                            { convertPathData: false }
                        ]
                    }
                }]
            }]
        }

My My.Assets module is identical to the one in the elm-assets-loader documentation:

module My.Assets exposing (AssetPath(..), path, star)

import Html exposing (..)


type AssetPath
    = AssetPath String


star : AssetPath
star =
    AssetPath "./star.svg"


path : AssetPath -> String
path (AssetPath str) =
    str

and my test view is:

textHtml : String -> Html msg
textHtml t =
    div
        [ Json.Encode.string t
            |> Html.Attributes.property "innerHTML"
        , style
            [ ( "width", "24px" )
            , ( "height", "24px" )
            ]
        ]
        []


view : Model -> Html Msg
view model =
    div []
        [ textHtml <| My.Assets.path <| My.Assets.star
        , text "hello"
        ]

This part is a little dirty to be able to inject the svg node, but I don’t know a better way without modifying elm-assets-loader. However this most likely won’t work with Elm 0.19 given the recent patch that forbids innerHTML, is there another way?

Still, this works fine, the svg file is processed through svgo then correctly inserted, I get:

<div id="main">
    <div>
        <div style="width: 24px; height: 24px;">
            <svg xmlns="http://www.w3.org/2000/svg">
                <path d="..."></path>
            </svg>
        </div>hello</div>
</div>

Pretty cool!

Now if only elm-assets-loader was compatible with webpack3, but maybe someone has began to update it?

PS: I’m not sure why @luke did not use this as this is a NoRedInk project.

Edit: I think the proper way to avoid injecting the SVG node using the innerHTML property in Elm would be to modify elm-assets-loader to make it supporting tagged Svg msg values in addition to String. In this case it would use innerHTML itself in javascript. I don’t think this would be too dangerous as this is done during the build.

Edit2: there is a fork updated for webpack3/4: https://github.com/alvivi/elm-assets-loader

We stopped using Webpack at NRI about a year ago (a little before I joined), so this is actually the first time I’ve seen this loader! The method I chose is primarily driven by a desire to let the browser cache the contents of the SVG sprite since I almost never change it even if I update the application code. I didn’t really see much utility in going through Elm to inject the SVG document into the DOM, and I skipped SVGO because I find it to be difficult to configure and I can do a reasonable job by hand.

At NRI we have a Python script that crawls a directory of SVG files and optimizes them with SVGO and combines them into a string that gets embedded into a JS file. The JS file injects the SVG content into the DOM, and we load the JS file asynchronously. This works well with our larger team and our typical strategy of downloading individual SVG files for icons that were originally added through Sketch by our design team. The Elm API looks a lot like the one in Ellie, except that there’s more stuff in it to account for legacy img tag icons and to provide correct accessibility markup for different usecases.

I’ve been planning the next iteration of the icon system for both Ellie and for NRI, and I think we’re going to take Chris Coyier’s most recent advice on the topic and just use inline SVG via elm-lang/svg. The SVG 2.0 spec indicates that use transcludes the content at the href via shadow DOM, which makes advanced styling stuff really frustrating. Plus, our designers edit Elm files all the time so making updates isn’t an issue.

2 Likes

Thank you very much for this detailed answer and your insight.

Maybe a transfer of elm-assets-loader to elm-community could be discussed if there is some interest there as it could still be very useful for those that use webpack (including to take Chris Coyier’s advice as can be seen)?

This could maybe make easier to get it updated to webpack3/4 and improved through contributions (edit: for example to merge with GitHub - alvivi/elm-assets-loader: webpack loader for webpackifying asset references in Elm code that is compatible with webpack3/4).

The only thing I am questioning in his advice is when he says:

It seems to me that this relies in his example on using PHP <?php include("file.svg"); ?> which is not available in all languages (maybe one day the Elm compiler will be able to parse directly SVG files?), and this eludes the question of the often needed processing after designers delivery.

Has anyone considered writing a python or node.js script that takes every SVG file in a directory, processes them through some configurable steps (typically svgo), then generates directly an Elm module (using html-to-elm or some kind of variation)?

PS: @Dan_Abrams, doing this through a Sketch plugin seems to restrict the usefulness unnecessarily.

To complete my previous experimentation, I used elm-svg-parser instead of the innerHTML property and this works fine (and should still work with 0.19 I think). I just replaced the view code by:

view : Model -> Html Msg 
view model =
    div []
        [ case SvgParser.parse <| My.Assets.path <| My.Assets.star of
            Ok svg ->
                svg 

            Err error ->
                text ("invalid svg: " ++ error)
        , text "hello"
        ]   

Of course the Result handling could be elsewhere, this is just a test.

So to answer initial @Sebastian’s question:

  1. You can use today elm-assets-loader, chain through svg-inline-loader and svgo-loader to clean the SVG and have the SVG inlined properly with elm-svg-parser (if you need webpack3/4 compatibility, use alvivi/elm-assets-loader).

  2. elm-svg-loader seems to offer a similar solution without using elm-assets-loader.

  3. Use an SVG sprite solution with the drawbacks listed before.

Edit: I think that a script that processes all SVG from a directory through a configurable step (svgo), then generates an Elm module might be simpler than all of those, but I don’t know one already developed.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.