In the previous widget building tutorial, I made a custom widget for my two new matter connected air filters. Well, they work well enough that I just got another one. Let’s put all three on one widget!
This is the second in a series of planned tutorials walking through many aspects of widget creation. This tutorial is going to be a deeper dive into some of the more technical aspects of widgets, so I’m going to assume that anyone reading this tutorial is already familiar with widget basics. If you are not, you can check out the first tutorial.
Custom widget tutorials
- How To Build A Custom Widget
- How To Build An Advanced Custom Widget
- How To Troubleshoot Custom Widgets - Coming Soon!
What can I do better?
My basic widget works well for…basic…functionality, but I’ve found a few places where it comes up short.
- The knob is nice looking but, it doesn’t scale dynamically.
- The fine speed control with a continuous input like a knob is nice, but I also want a few more presets of easy to access values.
- I’ve also added an new item to each of my filter Equipment Groups that tracks the date of the next required filter change and I want to see that at a glance.
- With a third filter I want to add to the mix, three separate cards seems like just too much, and I want to pack all three onto a single card.
It’s that last point that is going to push this from a basic widget to an advanced one because if I’m going to squeeze every last bit of space out of a card, the basic OH and F7 components probably won’t be enough.
I’m going to follow a very similar process to the basic widget. That means that the first thing I have to do is have a clear idea of where to start. Between the requirements from the basic version and the three new ones listed above I can mock up a few drafts to see what looks best. This time I’ll spare you the steps of the drafting process and go right to the final layout:
That looks right to me and it satisfies all the new requirements, but how do I translate that layout into widget components? I could use f7 rows and columns and the power of CSS flexbox to build this layout, but in this case, I think it would start to get hard to keep track of (for example, the filter and air quality components would be in columns in a row in a column in a row…). So, I’m going to prefer a more direct approach to layout, the CSS grid.
The idea behind the CSS grid is that I can overlay a series of columns and rows on my layout with one configuration and then define sets of grid cells that should be merged into individual regions. My overlay in this case looks like this:
Then I’ll just combine, for example, the three right side cells into one region for the vertical slider.
One last step before I head over to the widget editor: what is my root component going to be? I still want this widget to be a card, so like the basic version that root could be an f7-card, but this isn’t a basic widget, this is an advanced widget. That means, I expect I’m going to need complicated widget expressions throughout the widget. So, I’m going to make the root component an oh-context which will make writing and maintaining complex expressions much easier as the widget creation process goes on.
The info components: repeaters and context
There are two specialized OH specific components that don’t render any visible html on their own: the oh-repeater and oh-context.
These components are for organizing the information in a widget instead.
The repeater allows the widget builder to use the same components multiple times by looping through a list of information and causing its child components to be rendered once for each time it loops.
The repeater’s power comes from the fact that it can handle many different kinds of lists of data:
- Simple arrays - the source list for a repeater can be a simple array of information such as a number sequence or a list of strings.
- Complex objects - the source list can also be an array of objects meaning that each time the repeater loops the loop information contains all the properties of that object.
- Item lists - two source list options actually populate the repeater array with item objects using direct calls to the OH API so you can loop through all the items in a group or all the items with a specific combination of tags and have access to all the information about an item.
The context component is all about working with widget expressions.
It gives a user more control over the scope and default values for widget variables as well as the ability to define useful constants and simple utility functions that can be called in the widget expression of any of its child components.
Building the grid
I’m ready to start the widget yaml. So, I run through the basic widget configuration steps first and then add my root component (I’ll leave the oh-context config blank for now and populate it as I develop widget expressions later on). Then I add the f7-card as a child of the context component and build my CSS grid:
uid: demo_advanced_widget
tags: []
props:
parameters: []
parameterGroups: []
component: oh-context
config: {}
slots:
default:
- component: f7-card
config:
style:
display: grid
grid-template-areas: |
"select-tab select-tab speed-range"
"filter-icon air-qual speed-range"
"fan-mode fan-mode speed-range"
grid-template-columns: 40px 1fr 1fr
grid-template-rows: 30px 45px minmax(45px, auto)
The entire grid is defined by just four CSS properties.
display- Here we tell the card that we want it to arrange it’s child elements in a grid.grid-template columns- I want three columns in my grid and I want the first one to be 40px wide and I want the other two to each take up 1 fraction of the remaining space (fraction orfris a CSS size unit that only applies in very limited cases such as the grid).grid-template rows- In this case I give absolute heights of 30px and 45px to the first two rows and I tell the last row to be at least 45 pixels.grid-template-areas- This is where the magic happens; this property has to be a multi-line string (the|after the key in yaml is one way to start a multi-line string) with one line for each row. Then each line is a string of human readable labels, one for each column. When you use the same labels in more than one place in this area definition, you are combining those grid cells into one usuable region.
CSS grids can do a lot more than this simple arrangement and if you’re interested you can take a look at this guide to learn more.
Right now, however, there are no components in the card to place in the grid, so I’m going to add a series of simple placeholders to visualize the grid and make sure it’s what I want. This is the first place I’m going to look beyond what the pre-made options have for me.
Going beyond OH and F7
The Creating Personal Widgets help page has a subsection on the different available components. In the previous tutorial I discussed three of the four types (OH, F7, and Contents vs Labels). The fourth type is possibly the most important when it comes to advanced widgets, because we can include straight html as components. In effect, this means we can build, using the custom widget system, nearly anything that can be rendered with HTML.
You will never need to write raw html, and you will certainly not need to master the broad array of html concepts that go into modern web pages. However, one of the most helpful parts of the help doc mentioned above is that it shows a few examples how widget components are translated into HTML. If your goal is to move beyond the simplest of widgets, perhaps the most valuable skill to learn is an understanding of what components become what HTML.
How to speak HTML
All of HTML is, obviously, something I can’t tackle in one OH tutorial, but, I am going to use some common terms througout the rest of this post that I think are worth a brief introduction.
HTML (and some other similar markup languages) use the concept of
tagsto tell the computer how to understand the document. The different tags in html define elements, each with its own purpose and look. Any big block of text on a web page probably starts with a<p>tag which tells the HTML rendering program that text is just a plain old paragraph. But, those blue underlined words in the middle of the paragraph are not just any old text. Those words, of course, are a hyperlink and the rendering program knows that because they get surrounded by their own<a>tag. If you are typing text into a box on a page, that box is probably defined by an<input>tag.Tags and elements and nodes, oh my!
There are three common closely related words you’ll see when talking about HTML and two of them will be used often through this tutorial.
- Tag - this is the general form of an html entity denoted by the actual text inside the
< >. Tags are either opening tags such as<a>or closing tags</a>.
The tag defines the type of element.- Element - this refers to an entire specific entity that starts with an opening tag and ends with a closing tag and everything in between. Each individual element also has inside its opening tag specific data about that particular element.
- Node - this is very similar to element but includes the idea that the element is at a specific location in the HTML document. Any element placed inside (between opening and closing tags of) another element is said to be a ‘child node’ and the outside element is the ‘parent node’. One child node can, of course, be the parent of its own child node(s), and that continued hierarchy leads to the entire HTML tree.
With these pieces we can take this html:
<div> <p> Here is a paragraph full of text </p> </div>and describe it with, “The child node of that div element is a p element.”
Right now, the reason I want something that’s not an OH or F7 component is that I just want a simple element with no additional styles or functions at all; in HTML, that’s the div element. I’ve defined 5 different regions within my grid, so I’m going to add 5 div components and use the grid-area style to assign each one of them to one of the regions.
uid: demo_advanced_widget_in_progress
tags: []
props:
parameters: []
parameterGroups: []
component: oh-context
config: {}
slots:
default:
- component: f7-card
config:
style:
display: grid
grid-template-areas: |
"select-tab select-tab speed-range"
"filter-icon air-qual speed-range"
"fan-mode fan-mode speed-range"
grid-template-columns: 40px 1fr 1fr
grid-template-rows: 30px 45px minmax(45px, auto)
min-height: 120px
slots:
default:
- component: div
config:
content: Air quality
style:
grid-area: air-qual
background: yellow
- component: div
config:
content: Mode and speed buttons
style:
background: green
grid-area: fan-mode
- component: div
config:
content: Selection tabs
style:
background: lightblue
grid-area: select-tab
- component: div
config:
content: Range slider
style:
grid-area: speed-range
background: orange
- component: div
config:
content: Filter
style:
grid-area: filter-icon
background: red
That fits my layout pretty well and now I just need to fill each of those areas with the correct piece of the widget.
Custom Tabs
I’ll start with the tabs that will let me select which filter is being shown. Really, in the end, tabs are just buttons which cause different sets of information to be rendered. So, I just need a list of buttons and a way to distinguish the information based on which button has been pressed.
Getting a List
Let’s start with the list. I only have 3 filters, so I could just make three different buttons, one for each filter. That’s not very flexible, though (for example, it would be hard for me to share this widget on the Marketplace), and what if I get a fourth filter? This kind of flexibility is exactly what widget properties are for. Fortunately, it is very easy to use widget properties to get a list of items.
When you first create a widget, the editor is populated by a default widget config that includes an example props object with two parameters already set:
props:
parameterGroups: []
parameters:
- name: prop1
label: Prop 1
type: TEXT
description: A text prop
- name: item
label: Item
type: TEXT
context: item
description: An item to control
I’m just going to modify a few things on the default item parameter. The most important part is that it needs to actually be a list. With the current settings if I press the Set Props button I’m shown the standard single item selection list (radio buttons). I need to add the multiple: true configuration to this parameter, and then I get the multi-selector list (checkboxes):
So, with the configuration:
props:
parameters:
- context: item
description: The widget item list
label: Item
name: items
required: false
type: TEXT
multiple: true
I can select all three of my filter equipment items:
Widget parameter options
The documentation for all the various options for widget props and parameters is an area of active work at the moment. For now, there are a some forum threads that have gathered this information that can be used for reference. See the Resources section below for some links.
When you have set a parameter in the widget’s props object, you can access the value of the parameter in widget expressions using the props object: props.parameter_name. For most parameters the value of that object property is intuitive. In this case, it might not be intuitive, but, because I have used the multiple property, the value is an array of the item names:
['Filter_Library','Filter_MasterBedroom','Filter_TobysRoom']
This has two consequence:
- If I want just one of those item names I have to use array indexing, e.g.,
props.items[0] - Because it is already an array, it is ideal as the source of an
oh-repeater
Making the buttons
Without any additional configuration, an oh-repeater will render all its child elements inside a container.
<div> <-- Created by the repeater
<child1 /> <-- Elements in the repeater's default slot
<child2 />
<child... />
</div>
That means in this case I can just replace the placeholder div I put in the select-tab grid area with an oh-repeater. I just have to make sure that the repeater’s div gets assigned to the correct grid area. The repeater has a special property for adding CSS styles to the container it creates, containerStyle. This repeater’s config is fairly short then:
- component: oh-repeater
config:
containerStyle:
display: flex
grid-area: select-tab
for: filItem
in: =props.items
I added display: flex to the style because that will make arrangement and sizing of the buttons automatic and responsive to the card size.
For the buttons themselves, I only have a few requirements:
- The button should look like a tab, not a regular button.
- My space for the tabs is small, so the text in each button has to be short.
- Pressing a button should set a widget variable (
selected) to the index of the name of the selected filter so thatprops.items[vars.selected]returns the filter name.
The first step is just a few CSS styles. I’d like the buttons to stretch to fill the available space (the container is a flexbox, so this is just one CSS style): flex: 1 1 5% starts each button very small (5%) and grows each one at the same ratio so they are always the same size. All four button corners are round, but I want the bottom two to be straight: border-bottom-left-radius: 0 and border-bottom-right-radius: 0. Each one should have an outline to show the tab shape: border-right: solid 1px gray and border-top: solid 1px gray.
Step two is getting an abbreviated name for the text of each button. My repeater is looping through the array of item names which means that in widget expressions loop.filItem represents the item name string. My items all have a generic naming system of GeneralType_Specifier(_FunctionType). The specifier is the unique information I need and as you can see in the array above it is always in PascalCase so, a good abrreviated name would be just the capital letters from that specifier.
Widget Expressions
I briefly mentioned and used widget expressions in the basic tutorial. There I said expressions parallel javascript syntax. More accurately, the expressions are passed through a javascript syntax parser. This means that not only do widget expressions actually use javascript syntax, much effort has gone into making sure most javascript operators and methods are supported. You can always check the widget expression doc page, or track down the original parser library to see if those tell you that a particular javascript feature is available, but usually I just do a quick test in the widget code tester in the developer tools sidebar.
One feature available in widget expressions are javascript string methods. Checking my preferred javascript reference site, the match string method will allow me to use regex so I can extract the capital letters from my item names. I’m not going to cover regex here, but one solution splits the name into its parts, extracts an array of the capital letters in the second part and joins the capital letter array back together into a string:
text: =loop.filItem.split('_')[1].match(/[A-Z]/g).join('')
Here are my new tabs responding to card size changes:

Switchable Tabs
It’s time to turn the buttons into tabs that switch between my different filter items. There are many different ways to do this but they all have the same underlying idea. I will use the variable action to set a widget variable and each button will set that variable to a different value. Other components will then be able to access that value in widget expressions using the vars object.
In this case, I want easy access to both the index of the selected item name and the item name string itself. I’ll set the variable to the index number using one of the special variables available in the oh-repeater: if you append _idx to the loop variable it returns the current loop index value and if you append _src to the loop variable it returns the full source array the loop is using. So, I add:
action: variable
actionVariable: selected
actionVariableValue: =loop.filItem_idx
to the button.
To get quick access to the item name, I’ll take advantage of the oh-context for the first time. Widget expressions accept the javascript arrow notation for defining functions. Any arrow functions that we define as properties of the context’s functions property can then be called in other widget expressions:
component: oh-context
config:
functions:
currentItem: = () => props.items[vars.selected || 0]
With that config, fn.currentItem() will be evaluated in each widget expression and return the currently selected item name (or the first item name in the list if no selection has been made yet).
As an example of how to use this, I can now change the background (and bottom border) of my tab buttons to indicate which button is currently selected and which are “background tabs”.
background: =(loop.filItem == fn.currentItem())?('transparent'):('rgba(127,127,127,.3)')
border-bottom: =(loop.filItem == fn.currentItem())?('none'):('solid 1px gray')

Range Slider
Next, let’s put in the new range slider. The first steps are pretty easy.
The Slider Container
I’m going to keep the div that sits in that grid location as a basic container for the slider but change up the style just a little.
- component: div
config:
style:
box-sizing: border-box
grid-area: speed-range
height: 100%
max-width: 100%
padding-top: 5px
position: relative
width: auto
The only style declaration worth pointing out is the box-sizing. When an html element is rendered on a page, size values (such as height and width) by default only refer to the content area of the element (padding and the size of thick borders are excluded). This often leads to situations where an overall widget is larger than the creator is expecting. box-sizing: border-box changes this behavior so that explicit width and height sizes are calculated included padding and border size as well. This way I don’t loose 3 hours later trying to figure out why my container is 100% + 5 pixels in height (100% for content and an additional 5 pixels for the top padding), it will always be 5 pixels for the top padding and the remaining portion of the 100% for the content.
Modifying A Slider
The functional configuration for a slider is short, I need to link it to an item and turn it vertically. But, the oh-slider is based on the standard range slider in the f7 library, and that’s the narrow line with the movable knob. I want something that’s going to fill the entire grid area so that it is easiest to interact with and that’s going to require quite a few style modifications.
CSS Variables
Many CSS styles are set with a static value such as
redor120px, but CSS extends to many dynamic capabilities as well. This includes some functions (such as theminmax()function I used in the grid definition above) and variables. One common use for variables in CSS is in packages that want a unified appearance such as the F7 library of components. For example, the F7 library uses different colors for text in light and dark themes. If you want to customize a widget and have something in your widget match the text color, you could develop a widget expression to select the color based on the current theme, or you could just use the--f7-text-colorvariable which will always be set by the F7 library to a theme appropriate color.Variables are set by declaring them like any other css property in a component’s style object:
--my-css-width-var: 240px, and using the variable requires thevar()CSS function:width: var(--my-css-width-var).Every F7 component (and therefore the OH components as well) have numerous such predefined CSS variables and it’s always worth a quick check of the F7 CSS doc page when you are trying to customize a component to see if there is a variable you can take advantage of.
A vertical slider will automatically take up the full height of it’s container, so I just need to adjust the width, however, if I add width: 100% to the slider’s style object, I don’t see any change in the slider width:
The problem here is that the F7 range slider is a perfect example of a single component that creates multiple html elements.
This is the html created by a single f7-slider component in a widget:
<div class="range-slider range-slider-vertical">
<div class="range-bar">
<div class="range-bar-active" style="height: 0%;">
</div>
</div>
<div class="range-knob-wrap" style="bottom: 0px;">
<div class="range-knob">
</div>
</div>
</div>
We have a container div that is the root of the slider itself. Inside that container there are separate elements for the bar and the knob and each of those elements has its own child element. So, when I add width: 100% to the slider component, I’m just increasing the width of that root container element, I’m not changing the width of the bar. In order for the bar (the functional part of the slider) to also span the whole width of the space, I need to apply a style to the range-bar element.
The component’s style object will not do that but looking at the available F7 CSS for a range slider, I see that there is a CSS variable I can set, --f7-range-bar-size. Setting that variable to 100% in the slider’s style object applies that variable value to the parent container and all its descendants, so I can use that to adjust the width of the slider bar to 100% as well.
The exact same logic applies to the rest of the changes I want to make to the bar as well. I don’t want to see the grey slider background (--f7-range-bar-bg-color: transparent) and I want the slider corners to match the rounded card corners (--f7-range-bar-border-radius: 5px).
The next step is a little more challenging: I don’t want to see the knob (grabbing the slider is now quite easy given how wide it is), but there’s no F7 CSS variable to make the knob disappear (and, I can’t just use display: none in the slider’s style object or the whole thing disappears). In this case, I need to inject some custom CSS directly into the widget that will target the knob itself using the stylesheet property.
This is a property that can be added to most components and then given a value that is actual CSS. OH will then produce a new set of CSS style directives at that point which will apply to any of the child elements of the component with the stylesheet.
I might need more than just this one knob CSS, so I’m going to add this CSS at a higher point in the widget: the f7-card itself. Now, I just need to know how to create the CSS.
Every block of CSS (anything in curly brackets {...}) must have at least one “selector” that can identify one or more elements in the page. There are CSS selectors that can allow you target elements with just about any kind of criteria and the basics are listed in this handy reference, but this should be about as simple as CSS selectors get. Here, looking back at that html above, I see that the slider’s knob has a class of “range-knob” and nothing else has that class, so I can use that class to target just that element. CSS classes can be selected with a . in front of the class name, so my CSS to remove the knob is:
.range-knob{
display: none;
}
Translation: for any element with the class “range-knob” apply the style display: none. Note that the CSS syntax looks very much like the yaml style properties, but you must have the ; at the end or it will not be properly formatted CSS.
I also want a nice big label to show the value while sliding (because I don’t want scale steps), so I’m going to go through all the same CSS steps with range slider label variables. I’m also going to decrease the opacity of the whole thing so that the active bar is not quite so saturated and the result is this:

I could now declare this slider done, but there’s one thing I still don’t like. That’s a lot of uninteresting dead space on that side of the widget, especially when the slider is at a low value. I think the solution is to place an image of my filter in that space as well. That’s easy to do with an oh-image as a second child of the “speed-range” div component that looks like this:
- component: oh-image
config:
style:
height: calc(75% - 10px)
left: 50%
pointer-events: none
position: absolute
top: calc(50% + 10px)
transform: translate(-50%,-50%)
width: auto
url: /static/airp.png
Several of those styles are just about sizing the image. The most important parts of that configuration are:
height: calc(75% - 10px)uses one of the most important CSS functionscalc(). This function allows you to combine values with different units into a single appropriate value. In this case the height of the image is 10 pixels less than whatever 75% of the parent height is.position: absolutemeans that instead of having the position of this element calculated based on the parent settings and other children of the parent element, this element will be placed exactly where I tell it (using thetopandleftstyles) relative to the parent element.pointer-events: nonemeans that this element doesn’t respond to any pointer events. As far as the cursor (or finger) is concerned, this element doesn’t exist. So, even though this element is visually on top of my slider, I can still interact with the slider as if this image isn’t there.
I need to fix one last thing. I just put the image right over where the slider label appears when moving the slider. You will only see the label when the slider gets to the very top of the range. So, I want to change the label to be in a static position at the top of the slider instead of moving with the slider. There’s no simple configuration for this. There’s no F7 CSS variable (in fact, the F7 library definitely doesn’t want you setting the position of the label, because it dynamically calculates that value and sets the label’s bottom style as you move the slider). So, I’m going to have to add a second directive to my stylesheet.
A little trial and error and I can determine that the element I need to target is not the “range-knob”, but the “range-knob-wrap”, so I add:
.range-knob-wrap{
bottom: calc(100% - 45px) !important;
}
to the stylesheet property. Note that I’ve had to add an additional piece (!important) to the style declaration. I just said that the F7 library constantly sets and changes the position of the label. This keyword tells CSS to ignore attempts to overwrite this value so that the F7 library cannot change it and the label will remain in a static position:

Multi-buttons
In the bottom right of my widget, I’ve left room for two buttons: the same mode button as the basic widget and an additional button for several more preset fan speeds. There’s a specific F7 component for combining buttons into a single bar, the f7-segmented (it’s even in the F7 button reference page). The f7-segmented acts as a container for multiple buttons and simplifies some of the styling of those buttons. So, I can replace my placeholder div element with an f7-segemented component and add my previous button (with less text and a new icon) and a new fan speed button as children.
- component: f7-segmented
config:
raised: true
style:
grid-area: fan-mode
height: 100%
margin-left: 5px
slots:
default:
- component: oh-button
config:
action: options
actionItem: =fn.currentItem() + '_FanMode'
actionOptions: 0=Off,1=Low,2=Medium,3=High,5=Auto
iconF7: gear_alt_fill
style:
flex: 1 1 50%
height: 100%
text: Mode
- component: oh-button
config:
iconF7: gauge
style:
flex: 0 0 30px
height: 100%
The f7-segmented uses flexbox styling to organize it’s child buttons, so to each of the buttons I’ve added a flex style to control the relative button size. The three values in the flex style determine the rate at which an element can grow, the rate at which an element can shrink, and what the initial size of the element should be. So, my mode button starts at 50% of the width of the container and can both grow to fill any extra space, and shrink if it has to fit in smaller space. But, the speed button is fixed at 30px; it cannot grow or shrink.

Now, I want this speed button to offer a list of preset speeds: 25,50,75,100. I want to keep the style consistent with the mode button, so I could use the same action: options property that I used for the mode button, but the large action list that pops up seems a little excessive for the small numbers:
So, how do I combine the different speeds in to a smaller number of action lines?
F7 to OH
I know of no way to combine actions using OH’s built-in options action, so it’s time to move up a level and see what F7 has to offer. This OH feature is derived from the F7 Action Sheet, so I head over to that doc page and see that there is a feature that will do what I want: I can add the grid attribute to an f7-actions component to make it an “action grid”. But…how?
This is where learning to see the equivalence between HTML and OH widget components becomes critical. At the bottom of nearly every F7 help page is example code and an interactive demo generated by that code. In this case, because I am always using the Vue version of the help pages, that code is in Vue format and the first part of every Vue file is the HTML template (it’s not exactly HTML yet because Vue still has to render it, but it’s actually even better because it is closer to our widget system).
The part of the HTML Vue template for the demo that shows the action grid starts with:
<!-- Grid -->
<f7-actions :grid="true" :opened="actionGridOpened" @actions:closed="actionGridOpened = false">
<f7-actions-group>
<f7-actions-button>
<template #media>
<img src="https://cdn.framework7.io/placeholder/people-96x96-1.jpg" width="48"/>
</template>
<span>Button 1</span>
</f7-actions-button>
<f7-actions-button>
<template #media>
<img src="https://cdn.framework7.io/placeholder/people-96x96-2.jpg" width="48"/>
</template>
<span>Button 2</span>
</f7-actions-button>
...rest of template
This exposes the exact structure I need for my widget. I’ll start with an f7-actions component, and I need to set the grid property to true for this be formatted as a grid (I’m going to ignore the opened property because there’s an easier method for opening and closing). That component will have one f7-actions-group child for each row I want in my actions sheet and I want two rows, one for my speed buttons and one for a cancel button. Each group will have one f7-actions-button child per action that I want. The F7 components, of course, cannot interact directly with OH items, so to make that connection I will put an oh-link with an OH action: command in each f7-action-button.
So here’s the widget equivalent (using an OH repeater for the number buttons because they are identical except for the speed value and fragment: true in that repeater to remove the default div container it would otherwise create):
- component: f7-actions
config:
grid: true
slots:
default:
- component: f7-actions-group
slots:
default:
- component: oh-repeater
config:
for: speedVal
fragment: true
in:
- 100
- 75
- 50
- 25
sourceType: array
slots:
default:
- component: f7-actions-button
config:
slots:
default:
- component: oh-link
config:
action: command
actionCommand: =loop.speedVal
actionItem: =fn.currentItem() + '_FanSpeed'
text: =loop.speedVal
- component: f7-actions-group
slots:
default:
- component: oh-button
config:
text: Cancel
To control the opening and closing of the action sheet, I can take advantage of some built-in F7 properties that are found on many of the F7 (and therefore OH) components: actionsOpen. Similar properties exist for most F7 overlay and modal components (e.g., popupOpen), but as I’m using the f7-actions I want the actions version. This property accepts two types of values, but I always prefer to use the more specific one, a string containing a CSS selector. When the component with the actionsOpen property is triggered, the first element of that type on the page that matches the CSS selector is opened. A custom class added to the component is usually sufficient to identify your overlay component, so I’ll add a filter-speed-actions class to the f7-actions and I’ll add actionsOpen: .filter-speed-actions to the button that opens it (I’ll also add actionsClose: .filter-speed-actions to the cancel button on the overlay itself).
F7 Overlays
The F7 library includes several different types of components can be opened to appear on top of other things on the screen and disappear again without having navigated away from the current page. MainUI makes use of many of these, but there are three you are most likely to want to use in your custom widgets:
- Popup - Floating container in the middle of the screen (e.g., many of the MainUI configuration dialogs)
- Popover - Smaller than a popup and connected to a particular other component or location (e.g., the state displays when clicking on a pinned item in the developer sidebar)
- Sheet - Half-page or full-page area usually rolling in from the bottom of the screen (e.g., add-on details in the Add-on Store)
Now I can open my actions overlay and when I do I see that it is close, but not quite formatted the same as other OH action overlays:
It looks like I need to specify that my number buttons should be 1/4 width not 1/3 width and I need to change the text color and size. The question is, What text color and size?
Checking the F7 CSS reference list I find that grid action buttons get slightly different default styles (through CSS variables) than regular action buttons. For font size, for example, I need to set the grid button variable (--f7-actions-grid-button-font-size) to be the same as the regular button variable (--f7-actions-button-font-size).
Color is a little different. The buttons on the OH options overlay are not the default values, OH specifies instead that they should be blue for the options and red for the cancel button. One of the ways that the F7 library tries to simplify things for the user is with a preset palette of colors (you can see the list of them here). Most often these built-in F7 colors are used in the properties of F7 components and just referenced by the color name (e.g., the f7-button icon-color property), but if you need to reference these colors in specific CSS styles instead of looking up the hex codes, you can just use additional variables such as --f7-color-red. So, I need to add:
style:
width: 25%
--f7-actions-grid-button-text-color: var(--f7-color-blue)
--f7-actions-grid-button-font-size: var(--f7-actions-button-font-size)
to the number buttons and
style:
--f7-actions-grid-button-text-color: var(--f7-color-red)
--f7-actions-grid-button-font-size: var(--f7-actions-button-font-size)
to the cancel button. Now when I open it, I see:
Gradient Wedge
The air quality indicator which used to be fairly long text needs to be reduced to something that can easily shrink or grow while still conveying the information. To me, that sounds like a triangular indicator would do the trick. There’s no triangular OH or F7 component, so I’ll have to make one using CSS, and the style I want is clip-path. The clip-path style allows for many different shapes and templates to clip an element, but for a simple triangle we’ll use the polygon() function with just three points.
clip-path: polygon(0% 100%, 100% 0, 100% 100%)
I’ll add that and a nice gray background to the air quality div that’s already there and I get:
To add information to that I’ll put a second wedge div inside the first one, but with two differences:
- This one will be colored with a simple red → yellow → green gradient
- Instead of going to 100%, I need to clip the width of the triangle depending on the air quality item’s state.
The gradient is easy, that’s just a background that instead of grey is:
background: "linear-gradient(to left, #dd0000, #eeee00, #00dd00)"
The width percent of the triangle is a little more difficult. As a reminder, my air quality items give a state of 1 - 6 with 1 being best air quality (lowest ppm) and 6 being the worst. Calculating the percentage of the triangle from that is not difficult, it’s just the state divided by 6 times 100. However, I’m going to need to use this value three times in the clip path for this div and that’s going to be an awkward expression. It’ll be easiest if I use another function in my oh-context:
qualityPct: = () => (items[props.items[vars.selected || 0] + "_AirQuality"].numericState) / 6 * 100
The value for the clip-path style needs to be a string value. For me, the most readable way to build a complex string in widget expressions is the string template (backticks around the entire string with variables directly included inside ${...}). That makes the final expression:
clip-path: =`polygon(0% 100%, ${fn.qualityPct()}% ${100 - fn.qualityPct()}%, ${fn.qualityPct()}% 100%)`
Now we see good air quality as:
and less good air quality as:
Date Icon
The last piece to add to the widget is one to handle my new filter change date information. There’s a lot I want to pack into this one section.
I want to:
- See at a glance what the approximate status is (color and icon)
- Be able to get a readout of a more precise date if I want it (maybe a tooltip)
- Be able to set a new date when I change the filter
The need for a tooltip, some interaction, and an easy icon display tells me this will need to be an oh-button component. But, I also know that I’m going to need to convert from a date value to one of several status values at least twice in this widget which sounds like I need a reusable function in an oh-context. More than that, if this function is going to be able to call the basic currentItem function to get the correct date item associated with the selected filter, it can’t be in the same context component that already exists. This is because the properties of one particular context cannot reference other properties in that same context. So I’m going to need to give this button it’s own additional context. As I’ll be using a context anyway, I’m going to simplify my expressions even more by creating a constant object to hold the states, icons, and colors that I’m going to need.
- component: oh-context
config:
constants:
filter:
state:
- unset
- ok
- soon
- late
color:
late: FireBrick
ok: ForestGreen
soon: Gold
unset: gray
icon:
late: xmark_shield_fill
ok: checkmark_shield_fill
soon: exclamationmark_shield_fill
unset: shield_slash_fill
functions:
getFilterState: = () => (items[fn.currentItem() + '_FilterDate'].state != 'NULL') + dayjs().isAfter(dayjs(items[fn.currentItem() + '_FilterDate'].state).subtract(2,'w')) + dayjs().isAfter(dayjs(items[fn.currentItem() + '_FilterDate'].state))
Dayjs - working with date-time
That getFilterState function looks worse than it is. The widget expression can include a dayjs object. This object gives you access to all of the basic date-time manipulation and comparison methods found in the day.js library and some, but not all, of the library’s extra plugins. For example, a call to dayjs() alone returns the current date and time. Using daysjs(items.someDateTimeItem.state) will return the OH item’s state as a dayjs object that can be manipulated with further methods. So, by using the dayjs object, the getFilterState function just contains three boolean tests:
- Does the date item have a value -
(items[fn.currentItem() + '_FilterDate'].state != 'NULL') - Is the date less than two weeks away (actually implemented as, is the current date after 2 weeks before the filter date) -
dayjs().isAfter(dayjs(items[fn.currentItem() + '_FilterDate'].state).subtract(2,'w')) - Is the date after the current day -
dayjs().isAfter(dayjs(items[fn.currentItem() + '_FilterDate'].state))
In javascript expressions boolean values can be automatically cast to 0 and 1, and the getFilterState function just sums up the results of those three tests. So, the output of the function is 0 if the DateTime Item has no value (all 3 are false), 1 if the date is more than 2 weeks away (only test 1 is true), 2 if the date is less than two weeks away (test 1 and test 2 are true), and 3 if the date has passed (all three tests are true). If you look at the filter object, I’ve defined filter.state as an array that converts those function outputs to state strings. The other two parts of that filter object use the state string as a key for a CSS color value and an F7 icon name.
Another useful method of the dayjs objects is fromNow(). This will render the given date in a more human readable format. This is perfect for my button’s tooltip because something like “2026-05-02 21:16:15” becomes “in 3 months”.
The dayjs object can be used to create values which will be accepted as commands to OH DateTime Items as well. I’ll need to use this if I’m going to have my button update the filter change date when I click on it. In this case, I don’t want it to change the date to the current day, but the date six months from the day I am putting in the new filter. There was an example of the subtract() manipulation method in the function up above, but here I would want to use add() instead.
Advanced widget actions
The date-time work above covers most of what I need for my button configuration, but there’s one more important usability feature to consider: NOT pushing the button accidentally. The button with its icon is going to be fairly close to the filter mode button. Given that I’m often an oaf with big, clumsy, button-mashing fingers, I don’t want to reset the filter date when I’m actually just trying to turn the whole filter off. I’m going to put two safegaurds on this button.
Several years ago, I posted a widget to the marketplace which was a button with a delayed confirmation (again, to prevent myself from extra unwanted clicks). I don’t need to use that here because, in the intervening time, a confirmation configuration has been added to widget actions. This can be a simple string value that will be displayed in a default dialog, but you can also give a more complex JSON object (or equivalent yaml object) value to set many different options.
Secondly, any component that can be clicked to trigger widget actions has a second set of configurations as well, the taphold actions. These actions are triggered not by a basic click but by a long press (mobile) or right click (desktop).
I’m going to configure this button so that I have to long press the icon which brings up a dialog box requiring a second confirmation. That should be enough to prevent me from messing up the filter dates.
That makes the complete button configuration:
- component: oh-button
config:
iconF7: =const.filter.icon[const.filter.state[fn.getFilterState()]]
iconSize: 45
style:
color: =const.filter.color[const.filter.state[fn.getFilterState()]]
height: 100%
width: 100%
taphold_action: command
taphold_actionCommand: =dayjs().add(6,'M').second(0).minute(0).hour(0).toISOString()
taphold_actionConfirmation:
text: =`Set filter replace date to ${dayjs().add(6,'M').format('MMM D,
YYYY')}?`
title: New Filter Date
type: dialog
taphold_actionItem: =fn.currentItem() + '_FilterDate'
tooltip: =(items[fn.currentItem() + '_FilterDate'].state != 'NULL')?(`Change
filter ${dayjs(items[fn.currentItem() +
'_FilterDate'].state).fromNow().toLowerCase()}`):('No
filter change date set')

Whew! That was a lot. I don’t know if anyone will make it to the end here, but if you did, thank you, and I hope you found it worth your time. The only thing left for me to cover now is what to do when your widget doesn’t come out right…
Next up: How To Troubleshoot Custom Widgets
Full Final Widget Code
Click here to see the completed widget code
uid: demo_advanced_widget
tags: []
props:
parameters:
- context: item
description: The widget item list
label: Item
name: items
required: false
type: TEXT
multiple: true
parameterGroups: []
component: oh-context
config:
functions:
currentItem: = () => props.items[vars.selected || 0]
qualityPct: = () => (items[props.items[vars.selected || 0] +
"_AirQuality"].numericState) / 6 * 100
slots:
default:
- component: f7-card
config:
style:
display: grid
gap: 5px
grid-template-areas: |
"select-tab select-tab speed-range"
"filter-date air-qual speed-range"
"fan-mode fan-mode speed-range"
grid-template-columns: 40px 1fr 1fr
grid-template-rows: 30px 45px minmax(45px, auto)
padding-bottom: 5px
padding-right: 5px
stylesheet: |
.range-knob{
display: none;
}
.range-knob-wrap{
bottom: calc(100% - 45px) !important;
}
slots:
default:
- component: div
config:
style:
background: "#88888811"
clip-path: polygon(0% 100%, 100% 0, 100% 100%)
grid-area: air-qual
slots:
default:
- component: div
config:
style:
background: "linear-gradient(to left, #dd0000, #eeee00, #00dd00)"
clip-path: =`polygon(0% 100%, ${fn.qualityPct()}% ${100 - fn.qualityPct()}%,
${fn.qualityPct()}% 100%)`
height: 100%
width: 100%
- component: f7-segmented
config:
raised: true
style:
grid-area: fan-mode
height: 100%
margin-left: 5px
slots:
default:
- component: oh-button
config:
action: options
actionItem: =fn.currentItem() + '_FanMode'
actionOptions: 0=Off,1=Low,2=Medium,3=High,5=Auto
iconF7: gear_alt_fill
style:
flex: 1 1 50%
height: 100%
text: Mode
- component: oh-button
config:
actionsOpen: .filter-speed-actions
iconF7: gauge
style:
flex: 0 0 30px
height: 100%
slots:
default:
- component: f7-actions
config:
class: filter-speed-actions
slots:
default:
- component: f7-actions-group
slots:
default:
- component: oh-repeater
config:
for: speedVal
fragment: true
in:
- 100
- 75
- 50
- 25
sourceType: array
slots:
default:
- component: f7-actions-button
config:
color: blue
slots:
default:
- component: oh-link
config:
action: command
actionCommand: =loop.speedVal
actionItem: =fn.currentItem() + '_FanSpeed'
text: =loop.speedVal
- component: f7-actions-group
slots:
default:
- component: f7-actions-button
config:
color: red
slots:
default:
- component: oh-button
config:
actionsClose: .filter-speed-actions
class: actions-button
style:
text-transform: none
text: Cancel
- component: oh-repeater
config:
containerStyle:
display: flex
grid-area: select-tab
for: filItem
in: =props.items
slots:
default:
- component: oh-button
config:
action: variable
actionVariable: selected
actionVariableValue: =loop.filItem_idx
style:
background: =(loop.filItem ==
fn.currentItem())?('transparent'):('rgba(127,127,127,.3)')
border-bottom: =(loop.filItem == fn.currentItem())?('none'):('solid 1px gray')
border-bottom-left-radius: 0
border-bottom-right-radius: 0
border-right: solid 1px gray
border-top: solid 1px gray
flex: 1 1 5%
text: =loop.filItem.split('_')[1].match(/[A-Z]/g).join('')
- component: div
config:
style:
box-sizing: border-box
grid-area: speed-range
height: 100%
max-width: 100%
padding-top: 5px
position: relative
width: auto
slots:
default:
- component: oh-slider
config:
item: =fn.currentItem() + '_FanSpeed'
label: true
scale: false
style:
--f7-range-bar-active-bg-color: var(--f7-theme-color)
--f7-range-bar-bg-color: transparent
--f7-range-bar-border-radius: 5px
--f7-range-bar-size: 100%
--f7-range-label-bg-color: var(--f7-theme-color)
--f7-range-label-border-radius: 5px
--f7-range-label-font-size: 20px
--f7-range-label-font-weight: 400
--f7-range-label-size: 35px
opacity: 0.5
width: 100%
vertical: true
- component: oh-image
config:
style:
height: calc(75% - 10px)
left: 50%
pointer-events: none
position: absolute
top: calc(50% + 10px)
transform: translate(-50%,-50%)
width: auto
url: /static/airp.png
- component: div
config:
style:
grid-area: filter-date
height: 100%
margin-left: 5px
position: relative
width: 100%
slots:
default:
- component: oh-context
config:
constants:
filter:
state:
- unset
- ok
- soon
- late
color:
late: FireBrick
ok: ForestGreen
soon: Gold
unset: gray
icon:
late: xmark_shield_fill
ok: checkmark_shield_fill
soon: exclamationmark_shield_fill
unset: shield_slash_fill
functions:
getFilterState: = () => (items[fn.currentItem() + '_FilterDate'].state !=
'NULL') + dayjs().isAfter(dayjs(items[fn.currentItem() +
'_FilterDate'].state).subtract(2,'w'))
+ dayjs().isAfter(dayjs(items[fn.currentItem() +
'_FilterDate'].state))
slots:
default:
- component: oh-button
config:
iconF7: =const.filter.icon[const.filter.state[fn.getFilterState()]]
iconSize: 45
style:
color: =const.filter.color[const.filter.state[fn.getFilterState()]]
height: 100%
width: 100%
taphold_action: command
taphold_actionCommand: =dayjs().add(6,'M').second(0).minute(0).hour(0).toISOString()
taphold_actionConfirmation:
text: =`Set filter replace date to ${dayjs().add(6,'M').format('MMM D,
YYYY')}?`
title: New Filter Date
type: dialog
taphold_actionItem: =fn.currentItem() + '_FilterDate'
tooltip: =(items[fn.currentItem() + '_FilterDate'].state != 'NULL')?(`Change
filter ${dayjs(items[fn.currentItem() +
'_FilterDate'].state).fromNow().toLowerCase()}`):('No
filter change date set')
Resources In this Tutorial
| OH Help Docs | |
|---|---|
| Component Reference | A list of the OH specific widget components, and the properties that can be configured |
| Creating personal widgets | Introduction to the technical details of widget creation and the types of components available |
| Widget Action Examples | Detailed descriptions and yaml configuration examples for the different OH actions |
| Widget Expressions | Detailed look at widget expressions and the types of information available in the widget expression context |
| Forum Threads | |
| Widget parameter examples | A marketplace widget that demonstrates many different widget parameter configurations |
| Widget parameter descriptions | A forum tutorial covering some basic information about many parameter configuration options |
| F7 Help Docs | |
| F7 CSS Reference | A list of the CSS variables that apply to each F7 component and their default values |
| F7 Button Reference | Doc page for the F7 button which also includes the F7 Segmented button configurations |
| F7 Action Sheet Reference | Doc page for the F7 action button which also includes the necessary vue example |
| F7 Color Reference | Doc page for the F7 built in colors |
| Other Resources | |
| CSS Grid Guide | A deep dive into all the settings and capabilities of CSS grids |
| Javascript Reference | A handy quick reference for javascript objects and their methods that are (probably) available for use in widget expressions |
| CSS Selector Reference | A handy quick reference for the most common CSS selector patterns |
| Day.js Reference | Reference docs for the widget expression compatible date-time library day.js |
















