Tiles & Sheets
Every official widget is built from the same parts you get. Put them inside Widget.Content, set a colour, and the tile lays itself out, scales from 1×1 to 8×8 and answers a hold.
SDK 1.17 and up
The parts, tokens and sheets on this page need SDK 1.17. Raise your manifest’s sdkVersion floor to it.
<Widget tone="accent" gestures={gestures}>
<Widget.Content>
<Widget.Glyph icon="mdi:fan" />
<Widget.Head icon="mdi:fan" eyebrow="Auto" name="Bedroom Fan" active={isOn()} />
<Widget.Hero value={speed()} unit="%" art={<img src={fanArt} alt="" />} />
<Widget.Controls>
<Widget.Stepper label="Speed" onStep={step} />
</Widget.Controls>
</Widget.Content>
</Widget>
The parts
| Part | What it is |
|---|---|
Widget.Content |
The tile. With parts inside, it places every part below; accent recolours this tile and its sheet |
Widget.Head |
Icon, a small line above the name (eyebrow), the name, and optional chips (aside). active lights the icon in the tile’s colour; count stacks it for a group |
Widget.Hero |
The big reading: value, unit, a small line above it (sub) and the tile’s art in the corner |
Widget.Controls |
The control row: beside the value on a wide tile, under it on a narrow one, hidden on a short one |
Widget.Stepper |
Minus and plus |
Widget.Choice |
A row of icon options |
Widget.Action |
A control for something that leaves no state to show (run a scene, press a button, stop a blind). It ticks after the call succeeds |
Widget.Chip |
A small badge for the head’s aside |
Widget.Backdrop |
A picture behind the whole tile, under a scrim that keeps the words readable |
Widget.Layer |
Your own layer over the whole tile, edge to edge and behind the words: a photo, a scene, a chart band |
Widget.Glyph |
The icon large and faint in the corner: what a 1×1 shows, and what fills a big tile that has no art |
Each part hides, shrinks or moves on its own as the tile changes size. You never measure the tile to lay it out.
Colour
Set tone ("accent", "info", "success", "warning", "danger", "neutral") or color on <Widget>. Everything that carries the widget’s colour reads --widget-color: the head icon, the glyph, controls, a ticked action, the sheet. Use accent on Widget.Content for a colour that changes with state, like a lamp’s own colour.
Sizes
The shell provides one scale. Use these instead of pixels in your own CSS and your content grows with the tile the way the parts do:
| Token | For |
|---|---|
--widget-text-value |
A big reading |
--widget-text-name |
A name |
--widget-text-meta |
A small line above a name |
--widget-text-sub |
A small line next to a reading; labels |
--widget-text-caption |
The smallest labels: axis ticks, list rows |
--widget-control-h |
A control’s height |
--widget-grid-pad, --widget-grid-gap |
Spacing inside the tile |
--widget-icon-box, --widget-icon-glyph |
The head icon |
.my-label {
font-size: var(--widget-text-caption);
}
Confirming an action
Most actions show their own result: a light turns on, a blind moves. For the ones that do not, use Widget.Action: it runs your call and ticks when it succeeds (a failure shows the dashboard’s error toast).
<Widget.Action
icon="mdi:stop"
aria-label="Stop"
run={() => callService("cover", "stop_cover", {}, target())}
/>
When the whole tile runs the action (a tap on a scene tile), share one useConfirm() between the tile and its actions and pass confirmed to <Widget>. The head icon and the glyph turn into a check, so even a 1×1 shows it.
const confirm = useConfirm();
<Widget confirmed={confirm.any()} gestures={gestures}>
…
<Widget.Action icon="mdi:play" confirm={confirm} id="run" run={runScene}>
Run
</Widget.Action>
</Widget>
Sheets
What the tile has no room for (each lamp of a group, colours, modes, sources, the days ahead) goes in a sheet. Pass it to WidgetDialog as a render function, only when there is something extra:
<WidgetDialog
{...widgetDialogProps}
{...dialogProps}
title={name()}
configSchema={configSchema}
config={props.config}
onConfigSave={(c) => ctx.updateConfig(c)}
{...(hasExtras() ? { sheet: () => <MySheet /> } : {})}
/>
Holding the tile then opens the sheet: grown out of the tile on a wide screen, up from the bottom on a phone. It wears the tile’s icon and colour; pass tile={{ backdrop: pictureUrl }} to add the tile’s picture behind its head. A tile without a sheet answers a hold with a short note that it has no more controls. A press on one of the tile’s buttons stays the button’s, but holding it still opens the sheet. Edit mode opens its settings, and the tab row shows only with Developer Mode on.
Build the sheet from PanelSection, PanelRows, PanelRow, PanelEntityRow and PanelFacts, with ToggleGroup for choices and SwatchPicker, TemperatureBar and ColorDisc for colour. Sheets render outside your widget, so your CSS does not reach them; these parts carry their styling.
Daylight
useDaylight() reads sun.sun. Declare { "domain": "sun", "access": "read" } in your manifest or the build fails; existing installs ask the homeowner once when they update.
Updating an existing widget
- Raise
sdkVersionto include 1.17 and bump@glasshome/widget-sdk. - Rebuild the layout from the parts above, and replace your own pixel sizes with the tokens.
- Move what your Controls tab showed that the tile cannot into a
sheet, and drop your hold wiring.