---
title: "Widget Config API"
description: "Declare a widget's settings form with defineConfig and field.* — field kinds, type inference, and raw schemas."
canonical: https://glasshome.app/docs/widget-sdk/config
section: "SDK"
updated: 2026-07-02
---
# 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.

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

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

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

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