---
title: "Tiles & Sheets"
description: "Build a tile from the Widget parts, size it with the scale tokens, and give it a sheet of extras for the homeowner to hold open."
canonical: https://glasshome.app/docs/widgets/widget-sheets
section: "Build Widgets"
updated: 2026-09-27
---
# 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.

```tsx
<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                              |

```css
.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).

```tsx
<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.

```tsx
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:

```tsx
<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.