A visual container that clusters controls into a glass card with its own internal grid.
Schema
| Field | Type | Required | Description |
|---|---|---|---|
type |
"group" |
yes | Discriminator — must be "group" |
id |
string | yes | Unique identifier |
label |
string | no | Header text shown above the group. Set to null for no header |
position |
[row, col] | yes | Grid cell position within parent |
span |
[rows, cols] | no | Grid cells occupied. Default: [1, 1] |
grid |
grid-dimensions | yes | Internal grid dimensions for this group's children |
children |
Control Definition[] | yes | Controls and nested groups |
dynamic |
string | no | Event name for runtime-injected children |
visible |
VisibilityCondition | no | Show/hide based on another control's value |
hideBackground |
bool | no | Remove glass card background (default: false) |
pulse |
Pulse object | no | Flash a ring around the group when a live event lands |
Example
{
"type": "group",
"id": "bedroom",
"label": "Bedroom",
"position": [2, 0],
"span": [2, 2],
"grid": { "columns": 2, "rows": 2 },
"children": [
{ "type": "toggle", "id": "br-light", "position": [0, 0], "label": "Light" },
{ "type": "toggle", "id": "br-fan", "position": [0, 1], "label": "Fan" },
{ "type": "slider", "id": "br-dim", "position": [1, 0], "span": [1, 2], "min": 0, "max": 100, "label": "Brightness" }
]
}
Dynamic Groups
When dynamic is set, the group's children are replaced at runtime by content arriving over MeshSocket. The incoming broadcast must have msg_type matching the dynamic value and a children array.
{
"type": "group",
"id": "now-playing",
"label": "Now Playing",
"position": [0, 0],
"span": [3, 4],
"grid": { "columns": 4, "rows": 3 },
"children": [],
"dynamic": "player_state"
}
Pulse
Set pulse to make the group flash a ring whenever a server event arrives —
a lightweight way to visualise live data landing in a group without wiring a
control. The ring expands outward (e.g. data collected/leaving) or contracts
inward (e.g. data arriving/stored), then fades. An optional filter narrows
which payloads trigger it, matching keys the same way a Sync filter does.
| Field | Type | Description |
|---|---|---|
event |
string | Server event that fires the pulse |
color |
hex string | Ring colour (default: green 34C759) |
direction |
"outward" | "inward" |
Pulse direction (default: outward) |
filter |
object | Optional payload match, e.g. { "device": "thunk-app" } |
{
"type": "group",
"id": "gps-collector",
"label": "Live GPS",
"position": [0, 0],
"grid": { "columns": 2, "rows": 2 },
"children": [],
"pulse": { "event": "iCloudListen", "direction": "outward", "color": "34C759" }
}
Theming
A group renders as a glass card whose surface, corner radius, border, and padding come from the active theme (surfacePrimary, cornerRadius, borderColor, borderWidth, cardPadding). Set hideBackground: true for a transparent group (no card), e.g. a hero row of status lights. Per-control theme overrides on the children still apply inside the group.
Rendering
- Groups render as a glass card (frosted material + subtle border)
- The internal grid is independent of the parent grid
- Groups can be nested (a group's children can include other groups)
Related
- Control Definition — controls within groups
- Layout Config — the top-level structure
- grid-dimensions — Grid sizing
- Visibility — The
visiblefield - Recipe: Server-Driven (Dynamic) UI — Runtime injection
- Theming — Theme & appearance system