I just bought two new matter connected air purifiers (so far, I am quite pleased with them). It turns out, that there isn’t a built-in MainUI widget to display the function and controls for an air purifier, so I’m going to have to make one.
This is intended to be the first in a set of tutorials where I’m going to go through my process of widget creation. I’ve got two main goals here. First, I hope these tutorials help cast some light on the aspects of custom widget creation many users find difficult. Secondly, I want to showcase the many resources that are available for widget creators (novice and expert alike).
Custom widget tutorials
- How To Build A Custom Widget
- How To Build An Advanced Custom Widget - Coming Soon!
- How To Troubleshoot Custom Widgets - Coming Soon!
1. How do I know where to start?
There has been a lively recent debate about how much technical expertise an OH user needs to become proficient at creating widgets. It’s too long to recap here, but the central question became “what level of HTML and CSS understanding does a user need to be successful at widget creation”. The good news is that for basic widget creation the answer is almost none. The OH widget system presents many preconfigured options that get us up and running with almost no detailed configuration. But, before we can even get to the configuration step, we have to know what we want.
My purifiers present four channels to OH:
- WiFi connection strength (Number:Power)
- Air Quality (Number 1 = Good, 6 = Extremely Poor)
- Fan Speed (Number 0 - 100)
- Fan Mode (Number 0 = Off, 1-3 Manual modes, 5 = Auto…I don’t know what happened to 4, it’s a mystery)
I don’t really need the WiFi connection, but I would like the other 3 to be represented all in one widget. After Linking Items to those channels, I’m ready and I’ve got two initial decisions to make about how I add those Items to a widget: 1) How do I want each of those items to be rendered, 2) How to I want them arranged on my widget? Let’s start with question #1.
If you are not familiar with all options available, there’s an OH Component Reference page which takes you through them and shows examples. In this case, I’ve got some pretty simple choices.
- Air quality: This is just a measurement. I don’t need to interact with this Item at all, I just need to see the word telling me the current status. In widgets, displaying a simple word can be done in a few different ways. Two of the basic ways are with
LabelandContentcomponents. If you are interested in the technical details you can find them here, but what that technical description boils down to is this:- A
Labelcomponent can go just about anywhere, but is nothing but plain old text and you can expect to have to add all the special style and spacing yourself (we’ll get there, but not yet). - A
Contentcomponent just places the text inside of something else that might already have the style and spacing. So, for basic needs, that can be easier in simple situations.
- A
I expect I’ll want a Content for this basic (mostly) style-free widget.
-
Fan speed: This is a number I need to be able to adjust between 0 and 100. There are a few different options here. Perhaps the most familiar is just the range slider, but steppers (up and down buttons) also let us change numbers, the knob or curved slider is also an option, as is a box where I could just enter the number manually. The stepper seems like a poor choice for a full 0 - 100 range unless I set pretty big steps and the manual entry too much of a hassle on some screens. The slider takes up a lot of room and I want several other pieces on the widget too. So, I’m going to opt to use the knob for this one.
-
Fan mode: This is a number item, true, but it really is just a selection of a few preset options. I could make a list of those options and choose each one as needed, but again, that sounds like it takes up a lot of space on a widget. So, I’m going to use a button that when pressed presents the list of options separately.
Question #2 about how I want to arrange these pieces can often be addressed by making some quick sketches or mock-ups to get an idea. I know the Content and the oh-button are small and text-shaped (rectangular). The oh-knob takes up more of a square space. Let’s look at some options:
There’s no right answer here, all would work. Personally, however, I like option 2 the best. It keeps the two interactive elements (the button and the knob) spaced out a little to prevent clumsy me accidentally pressing one or the other. I also just find the horizontal layout a little cleaner.
I think I’m ready to start, so it’s time to head to the widget editor.
A Few Notes about YAML
YAML is a data serialization language. This is a fancy way of saying that the goal of YAML is to find a sweet-spot where humans can easily type and read something that computers can also easily convert into understandable data. You don’t need to be a YAML guru to work with YAML in OH, but, I find that thinking about it as a data structure does help. A few simple guidelines should get us where we need to be to work with the widget yaml in the editor:
- The basic building block is the
key: valuepair where key is the name of the data object and value is the data itself.- Rule #1 means that there must always be a
:in a yaml line (and there can only be one colon that is not part of other text).- If the data is simple (text or a number) then the data just follows the colon (
color: red).- If the data is complex (multi-part object or a list), the colon will end that line and the next line will be indented one level to denote the start of the complex data.
- From #4 it becomes clear that indents are part of the structure - things indented the same amount are generally grouped together as a combined data object (each line starts with a key) or a basic list (each line starts with a
-).
I’m not going to spend too much time with the top of the default text in widget editor for now. First things first, I always change the generated UID to something more meaningful (I’ll opt for demo_basic_widget here). I’m also not going to be using any widget parameters in this widget so I’ll replace the default list with an empty list (not required, but it does clean up the yaml a little). Beyond that we can skip right down to the first place where it says: component: f7-card. Now it’s time to start building the actual structure of the widget, starting with this root element.
2. The F7 Library
I mentioned up above all the OH specific widget components that are named oh-[something]. Now we see that the default configuration for a custom widget starts with a component that begins with f7- instead. MainUI is built using Framework7 (F7), a library of UI components with a baked-in, consistent style and easy to access functionality. After the OH widget docs, the F7 help docs are probably the next most common reference you will need when getting started. The first thing you will probably use the F7 docs for is just looking at examples of the components. At the bottom of each of the component help pages you will find two things: a scary looking block of code which you can ignore for now (that’s for the next tutorial), and a set of examples of the range of what that component will do which is helpful right now. So, I’ll check out the page for the f7-card, and looking at those examples, I think that the card will work nicely for my widget.
OH vs F7 Components
With a few exceptions, the OH components are just built directly from the F7 versions. The OH reference docs show you examples of many of those components with OH specific styles already added on. The most important difference, however, is that the F7 components cannot interact with OH entities such as Items.
Do you need a toggle that controls a Switch Item? You will have to use the
oh-togglefor that because thef7-toggledoes not have the underlying code to communicate with OH.So, for many of the truly interactive parts of your widgets, you will find you have to use the
oh-versions of the components, but for basic structure and organization, thef7-components will make it easy to quickly create something that matches the rest of MainUI in style.
The f7 components that will do the heavy lifting for you when structuring a widget are the block, the row, and the col. The f7-block is really intended to be what you use to present a standard block of text area, but functions just as well as a generic region of a widget and the f7-row and f7-col are often used together to get a pretty easy grid-like layout.
There are a few other specialized f7 structure components you might use as well. In this case, for example, because I’m using an f7 card, there are some structure components listed on the f7 card help page for the cards header, footer, and content areas. I’m not going to customize the header or the footer, but I am going to use an f7-card-content component to get the first level of basic style and spacing for the things I’m adding to the card’s content area.
3. Keep It Simple
If you look at the underlying html of a modern web page, it feels like there are 20 extra html steps for every one that seems to be actually visible on a page. Assuming it is a well designed page, then each one of those pieces should be doing one thing and the designers should be able to articulate that purpose: “this one groups all the dynamic information that needs to be displayed incoming or out going”, “inside the main group, that one groups all the incoming information”, “inside the main group, that one groups all the outgoing information”, etc.
The same should be true for an OH widget. You should really be able to look at each component and say what’s its specific function is: “I included this to…”. If you can’t say what it’s doing, you probably don’t need it. One good way make this a part of your widget design is to work backwards from your idea and articulate what you need first.
For my widget then I need to see if I can articulate how the layout I sketched fits the three main f7 layout components:
- I explicitly defined “Air quality” as content to go in some block of text: this will now clearly be an
f7-block - I want that
f7-blockto be in a column with the mode button - I want that two part column to be in a row with with knob
- I want that overall row to be in the content area of a card
We could do the same logic visually by drawing the structure we want right on our mock up:
In both cases what we can see here is that there’s no reason to over-complicate the structure. One of the reasons that UI library like F7 exist is that it can get pretty easy to get lost in the weeds of element position and spacing when building a web page. The f7 components are specifically designed to handle a lot of that work for you so it’s best to just let them do their job. There’s a very good chance this won’t be 100% perfect on the first try, but it’s going to be very close and will mostly like require minimal manual tweaking.
So, here’s my first attempt at the widget code:
uid: demo_basic_widget
tags: []
props:
parameters: []
parameterGroups: []
component: f7-card
config:
title: Air Purifier
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-row
slots:
default:
- component: f7-col
slots:
default:
- component: f7-block
slots:
default:
- component: Content
config:
text: Air quality
- component: oh-button
config:
text: Fan mode
- component: oh-knob
(Here's the same code fully annotated)
# Always give your widgets clear UIDs
uid: demo_basic_widget
# Tags do not impact the function a widget but can be used for
# filtering in the widget list page
tags: []
# Widget props will be covered in the advanced tutorial so we'll ignore them here
props:
# Because we don't need any props we can set the parameters to an empty list
parameters: []
# This is empty by default but would be used if we needed to organize
# many parameters
parameterGroups: []
# Our root component. There can only be one root component.
# Note this does not start with a '-' it is a property in the main widget
# data object.
component: f7-card
# The data object containing all the configurations specific to our root component
config:
title: Air Purifier
# Slots are how we put some components inside other components.
# All additional components must be inside the root component, so
# we use the root component's slots
slots:
# Some components have multiple different slots (e.g., 'header' or 'footer').
# 9 out of 10 times you just need the default slot. The help docs for the
# f7 components will tell you if there are other named slots available beyond
# just default.
default:
# Each slot will contain a list of components (note the '-' in front of
# 'component'). This component is the container for all the content
# we are adding to our card.
- component: f7-card-content
slots:
default:
# Inside the content container is our row container for the
# column and the knob.
- component: f7-row
slots:
default:
# Inside the row container is our column container for the
# block and the button.
- component: f7-col
slots:
default:
# The block is only for holding the `Air quality` content.
- component: f7-block
slots:
default:
- component: Content
config:
text: Air quality
- component: oh-button
config:
text: Fan mode
# The indentation of the knob matches the column
# container because they are both part of the same
# list; the children of the row container.
- component: oh-knob
And this is the result:
Yeah, by keeping it simple we got pretty close!
4. The Finishing Details
I see only a few things I want to tweak to get it closer to my original idea:
- I want the button to be a little more obviously a button.
- The knob is a little too big relative to everything else.
- The “Air quality” text needs to be a little bigger, centered over the button.
- The button and the text need a little more space between them.
Let’s start with the button. The first place to look for properties of the button that can be configured is the oh-button reference page. I like the look of the raised button example on that page and if I scroll down through the properties I find that raised is just a boolean property so I’m going to add raised: true to my button config.
Same process for the knob. When I check the reference page for the knob I see a size property. I’ll start with 100 as a size and may adjust it later. I also like the look of the rounded ends I see in the examples on that page so I’ll add lineCap: round to the knob too.
Customizing the “Air quality” text is where we run into our first need for some direct changes to the style of a component. I said at the top that we need to know almost no CSS for basic widgets like these. We certainly don’t need to know any of the intricate details, or even much of the formatting. I’d say that right now we need to know 2 things:
- To change the appearance of any piece of a web page we can apply directives to the style attribute of that piece.
- There are a lot of different style directives and it’s not worth your time trying to remember them all (I sure don’t). So, find a CSS style reference page that you like (my personal go-to reference is w3 schools CSS reference, but there are many good options) or ask your favorite search engine. Even your friendly AI helper should be 99.9% reliable when asked a basic question like, “what’s the css to center text?”.
I’ll stick with my reference doc. I’m trying to change some text, so I’ll type “text” into the search bar above the style list in my reference page and the first filtered result says:
text-align: Specifies the horizontal alignment of text
That sounds just about right, and clicking on the link takes me to a description of the text-align style. Turns out I need text-align: center.
I don’t see any text results about size, so I’ll change the search filter to “font” and there’s
font-size: Specifies the font size of text
Perfect. I’ll pick a random gut feeling size for the font and then we can adjust it once we see the effect.
To apply these to a widget component I’ll add a style object to that component’s config object. The key: value pairs inside that style object will just consist of the style name for the key and the style value for the value. There’s one extra wrinkle in this case: the content component is one of the few widget components that doesn’t take a style object (because it’s just text and not an actual HTML entity); instead the style will be applied to the block that contains that content.
- component: f7-block
config:
style:
font-size: 20px
text-align: center
slots:
default:
- component: Content
config:
text: Air quality
The spacing between elements on a web page is also handled by their styles. The most direct way to add space is the margin style which controls space outside the border of an element. To get space between the text and the button I could either add a margin below the text or above the button. Because I already have a style object for the text block, I’m just going to add the margin there and because I want it to be space below the text block, I’ll make it margin-bottom.
Click here for the full current widget text
uid: demo_basic_widget
tags: []
props:
parameters: []
parameterGroups: []
timestamp: Nov 14, 2025, 10:35:22 PM
component: f7-card
config:
title: Air Purifier
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-row
slots:
default:
- component: f7-col
slots:
default:
- component: f7-block
config:
style:
font-size: 20px
text-align: center
margin-bottom: 20px
slots:
default:
- component: Content
config:
text: Air quality
- component: oh-button
config:
raised: true
text: Fan mode
- component: oh-knob
config:
lineCap: round
size: 100
And here’s the result:
That’s good enough for me! Now that it looks right, we have to make it do something.
5. How do I make it do stuff?
This part’s usually pretty easy and there are three main ways you are going to have widget components interact with OH:
- Direct configuration - some
oh-components connect directly to OH Items and then changes in the widget are directly sent to the Item and changes in the Item are immediately reflected in the widget. - Widget action - other
oh-components accept configurations that set up a widget action, that is, some OH activity that is preformed when the user interacts (usually clicking on) with the widget. This can include action that change Items, but many other actions as well. - Widget expressions - Most places in the widget yaml, instead of a static value you can set the value to a dynamic expression which, if it includes information about an Item will update in real time as that Item changes.
The oh-knob is an example of the first type of OH interaction. This component has an item property and that property should be set to the name of an OH Item. I’ll add item: Filter_MasterBedroom_FanSpeed to the knob’s config object, and that’s all I need to do.
This type of configuration is two-way. The component both reads (and displays the current state of the Item) and can send commands to the Item as well to change the Item’s state.
The oh-button is an example of the second kind of interaction, the widget actions. In fact, these two are so closely linked, that the main reference information for the widget actions is part of the oh-button component reference page. When you look at those examples you see that the pattern is always the same; configuring an action requires setting the action property to the type of action you want and then configuring some number of additional properties that vary depending on the type of action. My fan mode item as described above is a series of options, so the action that makes the most sense here is the options action. Opening up the example on the actions reference page, I see that the options action will need actionItem and (optionally) actionOptions properties. Those are all separate config options so that looks like this for my button:
- component: oh-button
config:
raised: true
text: Fan mode
action: options
actionItem: Filter_MasterBedroom_FanMode
actionOptions: 0=Off,1=Low,2=Medium,3=High,5=Auto
Now, when I press the button my widget presents me with a pre-formatted list of options which will send the associated command to my fan mode Item.
The third kind of interaction, the widget expression, is required for the “Air quality” indicator (which at the moment is just static text). This is not the place to go over all the many things that widget expressions can do; there’s a comprehensive doc page for that. For now, I’m just going to focus on the most fundamental thing that a widget expression can do: give you access to some basic information about an Item.
All widget expressions begin with = and include syntax that parallels javascript. Right now you don’t have to know any javascript at all, because we’re just going to use an OH specific shortcut that always gives the State of the Item formatted how you’ve defined it in the Item’s state description metadata: @'Filter_MasterBedroom_AirQuality' (you can look in the link above to see exactly what this shortcut is doing).
Here’s the current state of the widget with the components all connected properly to OH.
Full widget code with actions
uid: demo_basic_widget
tags: []
props:
parameters: []
parameterGroups: []
timestamp: Nov 14, 2025, 10:35:22 PM
component: f7-card
config:
title: Air Purifier
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-row
slots:
default:
- component: f7-col
slots:
default:
- component: f7-block
config:
style:
font-size: 20px
text-align: center
margin-bottom: 20px
slots:
default:
- component: Content
config:
text: =@'Filter_MasterBedroom_AirQuality'
- component: oh-button
config:
raised: true
text: Fan mode
action: options
actionItem: Filter_MasterBedroom_FanMode
actionOptions: 0=Off,1=Low,2=Medium,3=High,5=Auto
- component: oh-knob
config:
item: Filter_MasterBedroom_FanSpeed
lineCap: round
size: 100
6. Can I change the color?
I want add one final touch to this widget that combines some of the pieces we’ve used up to this point. The black text for the air quality is a little dull. I think the color of the text should also reflect the scale of the air quality from “Poor” to “Good”? The widget expressions that I used up above can help here to, but first I need to know what property I have to use to set the color of that text. I know it can’t be a property of the content component (there are none other than text). I know some F7 components have text color properties so I check the F7 block help page, but I find no mention of a property for text color there either. If I go one step further up the widget tree to the column component, then any changes I make might also impact the button, which I don’t want. So, I’ve ruled out other options and now I know I have to use the block’s style object again and change the text color with a CSS property.
A quick search tells me that the CSS property for controlling the color of text is, conveniently, just color, and my helpful CSS reference tells me that color can accept a wide variety of formats to designate specific colors, including just using predefined color names. W3Schools has more than just CSS reference material and, in fact, there’s a page for all the predefined html colors you can use. To make this work in the widget, all I need to do is pick a color name and use that as the value for a color key in the block’s style object. For example:
- component: f7-block
config:
style:
font-size: 20px
text-align: center
margin-bottom: 20px
color: CornflowerBlue
![]()
But how do I use a widget expression to select a different color for each value of my air quality Item? This sounds like an advanced expression, but fortunately the OH expression doc page has a perfect example that I can adapt easily under the objects section. I just need to pick 6 different color names, and use them and the air quality text options to build a object in the expression and then reference the object with the current state of my air quality item.
- component: f7-block
config:
style:
font-size: 20px
text-align: center
margin-bottom: 20px
color: =({'Extremely Poor':'FireBrick','Very Poor':'DarkOrange',Poor:'Gold',Moderate:'YellowGreen',Fair:'GreenYellow',Good:'ForestGreen'})[@'Fitler_MasterBedroom_AirQuality'] || 'Black'
slots:
default:
- component: Content
config:
text: =@'Filter_MasterBedroom_AirQuality'
| Good | Moderate | Extremely Poor |
|---|---|---|
| |
|
|
I think that makes this widget complete! Thank you for reading this far. There’s plenty more to learn, if you are interested…
Next up: How To Build An Advanced Custom Widget
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 |
| F7 Help Docs | |
| F7 Vue Reference | A list of the F7 components available to a vue-based app (such as openHAB) and their configuration properties |
| Other Resources | |
| CSS Reference | A list of the CSS style options as well their basic use in addition to more advanced CSS topics |
| HTML Color Chart | A list of the regcognized named color for use in HTML and CSS |









