Custom SVG Icon Sets

The MainUI devs have done an amazing job making sure that when making your own custom widgets, you have access hundreds of possible icons, with the moderately recent addition of the iconify resource pushing that into the thousands. Even so, there are still times when only a custom icon will do. In these circumstances, OH gives you an ability to add your custom icons to the system to be called just as any of the other OH icons are. For example, my sonos speakers use a custom SVG icon that can be set as the default icon for an item:
image

These custom icons, however, have a few limitations: most notably that unlike the other new icon sets, these icons cannot be recolored by the user in the widgets.

Here I’m going to go through a mechanism to define an entire SVG icon library using only two MainUI custom widgets (one for the library and one to display the icons). As they rely exclusively on the widget system, these libraries can be shared in the widget marketplace. They can also be recolored just as the f7 etc. icon sets can. The icon display widgets themselves can be used anywhere the oh-icon and f7-icon components can be used in custom widgets.

SVG Sprites

This icon library system is based on SVG sprites. The drawing elements in an SVG file can be grouped together in mostly arbitrary ways, and these groups can be given unique identities. When an SVG constructed in this manner is added to a web page, then those individual groups or “sprites” can be referenced in-line as their own elements in the page.

The question then becomes, “How do we build an SVG with sprites in an OH widget?” This is actually easier than it sounds thanks to a special property of the f7-row components: tag. The tag property allows you to convert an f7-row component from it’s standard <div> element into any other element tag you wish, and this includes the tags that are part of the SVG syntax. So here’s the code for a widget that defines a library of three SVG sprites: spiral, tribar, and starblob.

uid: svg-icon-demo-library
tags: []
props:
  parameters: []
  parameterGroups: []
timestamp: Aug 28, 2022, 8:17:45 PM
component: f7-row
config:
  tag: svg
  style:
    display: none
slots:
  default:
    - component: f7-row
      config:
        tag: symbol
        width: 24
        height: 24
        viewBox: 0 0 24 24
        id: spiral
      slots:
        default:
          - component: f7-row
            config:
              tag: path
              d: m 11,13 c 0.48,1.37 -2.10,0.88 -1.86,-0.40 -0.26,-1.57 1.38,-2.77 2.84,-2.56 2.04,0.13 3.41,2.31 3.08,4.24 -0.27,2.53 -2.93,4.27 -5.37,3.91 -2.82,-0.24 -5.0,-2.90 -5.02,-5.66 -0.17,-3.02 2.02,-5.90 4.92,-6.68 3.33,-1.03 7.20,0.54 8.88,3.59 2.11,3.55 1.19,8.51 -2.02,11.10 -3.59,3.09 -9.41,2.87 -12.85,-0.34 -3.45,-3.03 -4.38,-8.36 -2.49,-12.5 1.72,-3.97 5.9,-6.59 10.19,-6.69 4.39,-0.21 8.78,2.16 10.99,5.96 2.76,4.55 2.38,10.72 -0.88,14.91
              fill-rule: evenodd
    - component: f7-row
      config:
        tag: symbol
        width: 24
        height: 24
        viewBox: 0 0 24 24
        id: tribar
      slots:
        default:
          - component: f7-row
            config:
              tag: path
              d: m 21.63,22.5 c -5.81,4.92 -2.25,-4.4 -9.41,-6.97 -7.16,-2.57 -10.34,6.88 -11.69,-0.61 -1.35,-7.49 4.93,0.25 10.74,-4.66 5.81,-4.92 -0.79,-12.39 6.38,-9.82 7.16,2.57 -2.69,4.14 -1.33,11.63 1.35,7.49 11.13,5.52 5.32,10.43 z
    - component: f7-row
      config:
        tag: symbol
        width: 24
        height: 24
        viewBox: 0 0 24 24
        id: starblob
      slots:
        default:
          - component: f7-row
            config:
              tag: path
              d: m 19.38,23 c -5.27,3.88 -0.73,-7.01 -7.28,-6.97 -6.55,0.04 -1.86,10.88 -7.19,7.06 -5.32,-3.81 6.44,-2.86 4.38,-9.08 -2.07,-6.21 -10.92,1.59 -8.94,-4.65 1.98,-6.24 4.71,5.24 9.98,1.36 5.27,-3.88 -4.89,-9.89 1.66,-9.94 6.55,-0.04 -3.53,6.1 1.8,9.91 5.32,3.81 7.9,-7.7 9.96,-1.49 2.07,6.21 -6.89,-1.47 -8.87,4.77 -1.98,6.24 9.77,5.13 4.5,9.02 z

So, what’s going on here:

  1. The first f7-row is using the “svg” tag so This element is going to be rendered as an SVG image (in fact, if you wanted to just incorporate an entire SVG image into a widget or page, you could do so in this manner). In this case, however, this image is never going to be seen on a page because the display style for the image is set to “none”. However, that means when this component is placed on a page in another widget, it invisibly makes the sprites that it contains available to the rest of the page/widget.
  2. Each “sprite” is defined by one f7-row with a tag of “symbol” and a child f7-row with a tag of “path”. There are plenty of good websites around which do a better job of explaining the SVG syntax than I can do here. The most critical thing to note is that each “symbol” get it’s own unique id. This is what will be used to call each individual sprite.
  3. You can add whatever advanced path properties you need to the path element. See, for example that the spiral sprite uses fill-rule: evenodd.

The Icon Component

Including an SVG sprite in a document is simply the process of defining another SVG element, but instead of drawing something new, taking advantage of the use tag to tell that element to use an already defined SVG (such as a sprite). Here is what the actual icon component looks like:

uid: custom-svg-icon
tags: []
props:
  parameters:
    - description: Custom icon
      label: Icon
      name: icon
      required: false
      type: TEXT
    - description: Icon Color
      label: Color
      name: color
      required: false
      type: TEXT
    - description: Icon Size
      label: Size
      name: size
      required: false
      type: TEXT
    - description: Icon Style
      label: CSS Styles
      name: style
      required: false
      type: TEXT
  parameterGroups: []
component: f7-block
config:
  style:
    display: contents
    --icon-size: =(props.size || "32px")
    --icon-fill: =(props.color || var(--f7-text-color))
  stylesheet: >
    .svg-icon {
       display: inline-block;
       fill: var(--icon-fill);
       width: auto;
       max-width: var(--icon-size);
       max-height: var(--icon-size);
     }
slots:
  default:
    - component: f7-row
      config:
        tag: svg
        class: svg-icon
        style: =props.style
      slots:
        default:
          - component: f7-row
            config:
              tag: use
              xlink:href: ="#" + (props.icon || "spiral")
              height: =(props.size || '32px')
              width: =(props.size || '32px')

There are several interesting things to note here:

  1. You can see the basic <svg><use>... structure as I described above with very few additional pieces. The svg component gets a specific class and some styling (more on that in a moment), and the use element has the xlink:href property that defines which sprite to choose (and sets a default, just in case no icon property is given and a default size).
  2. If you just load this widget into the widget editor, or a page and set its icon property you will not see anything. Why? The link to the SVG sprite is useless without the sprite library also in the same scope. The id specifying the sprite simply will not mean anything.
  3. Both of the f7-row components are inside an f7-block. This is entirely to allow extra style settings for the icon. Any settings put under the style property of the svg component will get applied to the icon. However, in order to make this component as flexible as the built-in icon components we want the user to be able to define those styles when using the widget. We could, add one property for each and every css style parameter and explicitly include all those options in the svg element, but there are way too many. So, instead, we define a single style parameter for the widget taking advantage of the fact that when the user sets the style in the standard way:
config:
  style:
    style parameters here...

it will be passed to our widget as an object. Then we can just set out internal style property to that object. This is where the extra f7-block comes in. There are style parameter that we want to set on the icon initially (especially the fill style which lets us recolor the icon as we see fit). However, there’s no good way in the widget expression parser to concatenate two objects (the user styles object and the object of our internal styles). To get around this we can harness the incredible power of the stylesheet property which lets us apply styles to the svg element from another block. However, we don’t really want that extra block in our widget so we trick it into disappearing by setting its own style to display: contents a setting that means the element just exposes its contents to its parent element. Now we can style the svg element with our required settings and let the user pass their own settings in.

Using the Library

Here’s an example of how to include and use these custom icons. We’ll create a list card and we’ll make sure to include the library component in the widget. Then we’ll put each of the custom icons in its own list item.

uid: demo:use_custom_icons
tags: []
props:
  parameters: []
  parameterGroups: []
component: oh-list-card
config:
  title: "Demo: SVG icons"
slots:
  default:
    - component: widget:svg-icon-demo-library
    - component: f7-list-item
      config:
      slots:
        before-title:
          - component: widget:custom-svg-icon
            config:
              icon: spiral
        title:
          - component: Label
            config:
              text: Spiral icon
    - component: f7-list-item
      config:
      slots:
        before-title:
          - component: widget:custom-svg-icon
            config:
              icon: tribar
              color: green
        title:
          - component: Label
            config:
              text: Green Tribar icon
    - component: f7-list-item
      config:
      slots:
        before-title:
          - component: widget:custom-svg-icon
            config:
              icon: starblob
              color: =(2 == (1+2))?("yellow"):("#0099ff")
        title:
          - component: Label
            config:
              text: Dynamic color starblob icon
    - component: f7-list-item
      config:
      slots:
        before-title:
          - component: widget:custom-svg-icon
            config:
              icon: spiral
              size: 48px
              color: red
              style:
                outline: 2px solid blue
                padding: 3px
                transform: rotate(90deg)
        title:
          - component: Label
            config:
              text: Large spiral icon with custom stylings

The end result looks like this:
image

Final Thoughts

If you want to use this system to create your own library of custom icons, it should be fairly straight forward. The only tricky part, if you are unfamiliar with working with SVG graphics will be getting the paths correct. I used inkscape to quickly draw the icons and extracted the d property of the paths through the xml viewer. Inkscape’s coordinate system is a little different so it took some manual tweaks of the initial move command to get the icons centered properly in the viewbox, but not much.

If you are going to incorporate more than one widget on a page that uses the custom icons then make sure that you put the library on the page itself instead of in each individual widget. I suspect that having multiple copies of the library will result in unexpected errors as you will then have multiple sprites with the same `id’ (not to mention extra load time on the page as it processes redundant svg elements).

The one place where these SVG library icons are less useful than the f7, material…etc. icon sets is that they cannot be set using the built-in icon properties of the OH widgets (e.g., oh-label-cell). If you wish to use these you will have to construct your widgets such that they rely on separate icon components, not the built-in icon properties and in the list card example above.

9 Likes

Extremely hacky way to display dynamic icons. Very hard to create and edit and possibly slowing down poor mobile browser. But:
truly dynamic icons
potentially faster than standard icons if reused multiple times on a page
possibly removes the need of creating and loading multiple icons to display items states, like gauge icon possibly could reflect literally every percent of humidity, from 1 to 100.

I hadn’t considered this idea, yet. Interesting. You certainly could make the icons themselves dynamic or responsive with this system. Widget expressions should work in the d property of the path. I suspect the only technical difficulty would be refreshing the page render when the path changes. It just might be worth looking into.

You could even make the icons animated, if you wanted to, I suspect. Yannick has posted an example of an animated SVG using the same f7-row technique. I don’t know how animations work with the symbol subdivisions, but I have no reason to expect that it wouldn’t work.

Hard to create, certainly. At least the first time or so. I don’t suspect there would be too much of a performance hit even on a lower end mobile device. My understanding is that SVGs are, as far as images go, fairly low resource.