Skip to content
Widget Dev pages

Updated

View MarkdownOpen in ChatGPTOpen in Claude

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

  1. Raise sdkVersion to include 1.17 and bump @glasshome/widget-sdk.
  2. Rebuild the layout from the parts above, and replace your own pixel sizes with the tokens.
  3. Move what your Controls tab showed that the tile cannot into a sheet, and drop your hold wiring.