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:
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:
- 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 thedisplay
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. - Each “sprite” is defined by one
f7-row
with atag
of “symbol” and a childf7-row
with atag
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 uniqueid
. This is what will be used to call each individual sprite. - 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:
- You can see the basic
<svg><use>...
structure as I described above with very few additional pieces. Thesvg
component gets a specific class and some styling (more on that in a moment), and theuse
element has thexlink:href
property that defines which sprite to choose (and sets a default, just in case no icon property is given and a default size). - 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. - Both of the
f7-row
components are inside anf7-block
. This is entirely to allow extra style settings for the icon. Any settings put under thestyle
property of thesvg
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 thesvg
element, but there are way too many. So, instead, we define a singlestyle
parameter for the widget taking advantage of the fact that when the user sets thestyle
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:
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.