Moving SVGs out of the file system and into regular Elm code can make icons easier to manage, especially if you find you need to make accessibility improvements.Imagine we have an arbitrary SVG file straight from our Design team's tools:<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewbox="0 0 21 21" style="enable-background:new 0 0 21 21;" xml:space="preserve"><style type="text/css">
.st0{fill:#FFFFFF;stroke:#146AFF;stroke-width:2;}
</style><title>star-outline</title><desc>Created with Something Proprietary 123.</desc><g id="Page-1"><g id="star-outline"><path id="path-1_1_" class="st0" d="M11.1,1.4l2.4,4.8c0.1,0.2,0.3,0.4,0.6,0.4l5.2,0.8c0.4,0.1,0.7,0.4,0.6,0.8
c0,0.2-0.1,0.3-0.2,0.4l-3.8,3.8c-0.2,0.2-0.2,0.4-0.2,0.6l0.9,5.3c0.1,0.4-0.2,0.8-0.6,0.8c-0.2,0-0.3,0-0.5-0.1l-4.7-2.5
c-0.2-0.1-0.5-0.1-0.7,0l-4.7,2.5c-0.4,0.2-0.8,0.1-1-0.3c-0.1-0.1-0.1-0.3-0.1-0.5l0.9-5.3c0-0.2,0-0.5-0.2-0.6L1.2,8.7
c-0.3-0.3-0.3-0.8,0-1c0.1-0.1,0.3-0.2,0.4-0.2l5.2-0.8c0.2,0,0.4-0.2,0.6-0.4l2.4-4.8c0.2-0.4,0.6-0.5,1-0.3
C10.9,1.1,11,1.3,11.1,1.4z"></path></g></g></svg>Notice that there's lots of extraneous information in the SVG - including some information that's distinctly unhelpful! The title of the SVG ends up being used as the accessible name of the SVG - it's more or less equivalent to an img tag's alt. A title of "star-outline" will not help our users to understand what this icon is supposed to represent.Compare the raw SVG value to what it might look like if rewritten as Elm code and tidied-up by a human developer:import Svg exposing (..)
import Svg.Attributes exposing (..)
starOutline : Svg msg
starOutline =
svg
[ x "0px"
, y "0px"
, viewBox "0 0 21 21"
]
[ Svg.path
[ fill "#FFF"
, stroke "#146AFF"
, strokeWidth "2"
, d "M11.1,1.4l2.4,4.8c0.1,0.2,0.3,0.4,0.6,0.4l5.2,0.8c0.4,0.1,0.7,0.4,0.6,0.8 c0,0.2-0.1,0.3-0.2,0.4l-3.8,3.8c-0.2,0.2-0.2,0.4-0.2,0.6l0.9,5.3c0.1,0.4-0.2,0.8-0.6,0.8c-0.2,0-0.3,0-0.5-0.1l-4.7-2.5 c-0.2-0.1-0.5-0.1-0.7,0l-4.7,2.5c-0.4,0.2-0.8,0.1-1-0.3c-0.1-0.1-0.1-0.3-0.1-0.5l0.9-5.3c0-0.2,0-0.5-0.2-0.6L1.2,8.7 c-0.3-0.3-0.3-0.8,0-1c0.1-0.1,0.3-0.2,0.4-0.2l5.2-0.8c0.2,0,0.4-0.2,0.6-0.4l2.4-4.8c0.2-0.4,0.6-0.5,1-0.3 C10.9,1.1,11,1.3,11.1,1.4z"
]
[]
]Example 1 Ellie linkOnce the SVG is rewritten in Elm, we can leverage the Elm type system to guarantee that icons in our application are always rendered in a consistent way. By exposing the Icon type but not exposing the Icon constructor, we can ensure that there's only one way to produce HTML from an Icon. This strategy is the opaque type pattern, which you can learn more about in former NoRedInk engineer Charlie Koster's blog post series on advanced types in Elm and in the Elm Radio podcast's Intro to Opaque Types episode.module Icons exposing (Icon, toHtml, starOutline)
type Icon =
-- `Never` is used here so that our Icon type doesn't need a type hole. Essentially, the `Never` is saying "this kind of Svg cannot produce messages ever"
Icon (Svg Never)
toHtml : Icon -> Html msg
toHtml (Icon icon) =
-- "Html.map never" transforms `Svg msg` into `Svg Never`
Html.map never icon
starOutline : Icon -- notice the type changed
starOutline =
svg
...
|> IconNow that we've got consistently-rendered icons, we can start thinking about what an accessible way to render the SVGs might be. Carie Fisher's article Accessible SVGs - Perfect Patterns For Screen Reader Users is the resource to use when considering how to render SVGs in an accessible way. We will be using Pattern 5, <svg> + role="img" + <title>, to ensure that our meaningful icons have appropriate text alternatives. (You might also want to read through the WAI's Images Tutorial if you haven't already - it might surprise you!)We need to conditionally insert a title element into the list of SVG children - which means we need to change how we're modeling Icon. We can easily & safely do this, though, because we've used an opaque type to minimize API disturbances.type Icon
= Icon
{ attributes : List (Attribute Never)
, label : Maybe String
, children : List (Svg Never)
}
toHtml : Icon -> Html msg
toHtml (Icon icon) =
case icon.label of
Just label ->
svg icon.attributes (Svg.title [] [ text label ] :: icon.children)
|> Html.map never
Nothing ->
svg icon.attributes icon.children
|> Html.map never
emptyStar : Icon
emptyStar =
Icon
{ attributes =
[ x "0px"
, y "0px"
, viewBox "0 0 21 21"
]
, label = Nothing
, children =
[ Svg.path
[ fill "#FFF"
, stroke "#146AFF"
, strokeWidth "2"
, d "M11.1,1.4l2.4,4.8c0.1,0.2,0.3,0.4,0.6,0.4l5.2,0.8c0.4,0.1,0.7,0.4,0.6,0.8 c0,0.2-0.1,0.3-0.2,0.4l-3.8,3.8c-0.2,0.2-0.2,0.4-0.2,0.6l0.9,5.3c0.1,0.4-0.2,0.8-0.6,0.8c-0.2,0-0.3,0-0.5-0.1l-4.7-2.5 c-0.2-0.1-0.5-0.1-0.7,0l-4.7,2.5c-0.4,0.2-0.8,0.1-1-0.3c-0.1-0.1-0.1-0.3-0.1-0.5l0.9-5.3c0-0.2,0-0.5-0.2-0.6L1.2,8.7 c-0.3-0.3-0.3-0.8,0-1c0.1-0.1,0.3-0.2,0.4-0.2l5.2-0.8c0.2,0,0.4-0.2,0.6-0.4l2.4-4.8c0.2-0.4,0.6-0.5,1-0.3 C10.9,1.1,11,1.3,11.1,1.4z"
]
[]
]
}Example 2 Ellie LinkAt this point, the internals of Icon can handle a text alternative being present, but there's no way from the API to actually set the text alternative . We need to expose a way to set the value of the label of emptyStar and any other icons we later create.We could change emptyStar to take a Maybe String, or we could add a withLabel : String → Icon → Icon helper, or we could move the label value off of the Icon type and thread the value through toHtml.At NoRedInk, we use the withLabel : String → Icon → Icon pattern, but any of these patterns could work.withLabel : String -> Icon -> Icon
withLabel label (Icon icon) =
Icon { icon | label = Just label }Once we have a way to add the text alternative to the Icon, we're ready for the "role" portion of the <svg> + role="img" + <title> pattern.import Accessibility.Role as Role -- this is from tesk9/accessible-html
toHtml : Icon -> Html msg
toHtml (Icon icon) =
case icon.label of
Just label ->
svg (Role.img :: icon.attributes)
(Svg.title [] [ text label ] :: icon.children)
|> Html.map never
Nothing ->
svg icon.attributes icon.children
|> Html.map neverExample 3 Ellie LinkNow, our functional icons should render nicely with a title and with the appropriate role! (Although if we were supporting Internet Explorer, we would also want to add focusable=false)We still have the decorative icons to consider, though. We can mark these decorative icons as hidden in the accessibility tree so that assistive technologies know to ignore it with aria-hidden=true:toHtml : Icon -> Html msg
toHtml (Icon icon) =
case icon.label of
Just label ->
svg (Role.img :: icon.attributes)
(Svg.title [] [ text label ] :: icon.children)
|> Html.map never
Nothing ->
svg (Aria.hidden True :: icon.attributes)
icon.children
|> Html.map neverExample 4 Ellie LinkAnd that's it!Over time, it's likely that an Elm-based SVG icon library will need to support more and more customization: colors, styles, attributes, animations, hover effects. All of these and more can be added to the Icon opaque type, without breaking current icons.There we have it: tidy icon modeling leading to tidy icons.Tessa Kelly@t_kelly9
NoRedInk is a California-based digital writing platform that provides solutions such as interest-based curriculum, adaptive exercises and actionable data to students.