Widget Config API
Declare a widget’s settings form with defineConfig and the field.* builders. Pass the result to defineWidget as configSchema; the dashboard renders the edit form automatically.
import { defineConfig, field, type Infer } from "@glasshome/widget-sdk";
const configSchema = defineConfig({
title: field.title(),
entityIds: field.entities("light"),
showBrightness: field.toggle({ title: "Show brightness", default: true }),
});
type Config = Infer<typeof configSchema>;
Infer<typeof configSchema> is your config type — use it for props.config.
Field kinds
| Field | Renders | Config type |
|---|---|---|
field.title() |
Text input (“Title”, optional) | string | undefined |
field.text({ title, description?, default? }) |
Text input | string (optional without a default) |
field.number({ title, description?, min?, max?, default? }) |
Number input | number |
field.toggle({ title, description?, default? }) |
Switch | boolean |
field.choice(values, { title, description?, default? }) |
Select | the literal union of values |
field.entities(domain, { title?, description?, deviceClass? }) |
Multi entity picker | string[] |
field.entity(domain, { title?, description?, deviceClass? }) |
Single entity picker | string[] |
field.area({ title? }) |
Area picker | string | undefined |
field.icon({ title?, default? }) |
Icon picker | string (optional without a default) |
field.stringList({ title, description? }) |
String list | string[] |
field.group(shape, { title }) |
Nested group | the nested object |
A default makes the property required (it always fills in); omit it to make the property optional. field.choice keeps the exact string-literal union, so props.config stays fully typed.
const configSchema = defineConfig({
mode: field.choice(["auto", "heat", "cool"], { title: "Mode", default: "auto" }),
target: field.number({ title: "Target °C", min: 5, max: 35, default: 21 }),
sensors: field.entities("sensor", { deviceClass: "temperature" }),
icon: field.icon({ title: "Icon", default: "mdi:thermostat" }),
});
// Config = { mode: "auto" | "heat" | "cool"; target: number; sensors: string[]; icon: string }
Icons are names, not imports
field.icon() stores an Iconify name like mdi:lightbulb. Render it with
<Icon icon={props.config.icon} /> from @iconify-icon/solid: the host
provides it, so a widget never bundles an icon set. GlassHome serves icon data
from its own origin and caches it, which is what keeps a widget’s CSP free of
third-party hosts.
One level deep
The settings form is a flat list of fields. field.group is the single
nested exception; don’t nest beyond that.
Advanced: raw schemas
For validation field.* can’t express (custom checks, unions), import z and use it directly — defineConfig accepts raw schemas alongside fields:
import { defineConfig, field, z } from "@glasshome/widget-sdk";
const configSchema = defineConfig({
title: field.title(),
webhook: z.string().url().meta({ title: "Webhook URL" }),
});
Migrating to 1.4.0
1.4.0 replaced the old config helpers (widgetFields.* and raw z.object) with defineConfig + field.*. Existing widgets keep working; migrate when convenient. New widgets can skip this section.
widgetFields is deprecated
widgetFields.* and building configSchema with raw z.object({ ... }) still
work, but they’re deprecated and will be removed in a future major release.
bun widget build warns on each use, so you can migrate when convenient.
The same applies to naming a field areaId to get an area picker. The host
used to infer one from the property name, so any other name silently rendered
a text input. Declare field.area() instead — it works under any name.
Run the codemod from your widget project:
bun widget migrate config # migrate every widget
bun widget migrate config --dry # preview without writing
bun widget migrate config --name clock
Anything it can’t safely rewrite is left in place and reported, so nothing breaks silently.
| Before | After |
|---|---|
widgetFields.title() |
field.title() |
widgetFields.entityIds(domain, opts?) |
field.entities(domain, opts?) |
widgetFields.singleEntity(domain, opts?) |
field.entity(domain, opts?) |
widgetFields.areaId() |
field.area() |
z.object({ ... }) |
defineConfig({ ... }) |
z.infer<typeof configSchema> |
Infer<typeof configSchema> |