Skip to content

Updated

View MarkdownOpen in ChatGPTOpen in Claude

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>