---
title: "Widget SDK"
description: "The GlassHome widget SDK API. defineWidget, Widget components, hooks, entity bindings, and theming."
canonical: https://glasshome.app/docs/widgets/widget-sdk
section: "SDK"
updated: 2026-10-04
---
# Widget SDK

`@glasshome/widget-sdk` (on npm) provides `defineWidget`, the [Config API](/docs/widgets/widget-sdk/config) (`defineConfig` + `field.*`), composable UI components, reactive hooks, the Home Assistant data layer, and theming utilities for SolidJS widgets.

The runtime constant `SDK_VERSION` equals the installed package version; the host compares it against each widget's `manifest.sdkVersion` range.

This page is the **guide**. For every public export across the three entry points, see the [API Reference](/docs/widgets/widget-api-reference).

## On this page

- [`defineWidget`](#definewidget) and [manifest fields](#manifest-fields)
- [`widgetFields`](#widgetfields) and the [complete widget example](#complete-widget-example)
- [`<Widget>`](#widget-component): [variants](#variants) and [slot subcomponents](#slot-subcomponents)
- The [`<WidgetDialog>`](#widgetdialog-component) settings dialog
- [Hooks](#hooks), [Home Assistant data & services](#home-assistant-data--services), the [`EntityView`](#the-entityview-shape) shape
- [Errors and empty states](#errors-and-empty-states), [theming](#theming), [entity utilities](#entity-utilities), [build tooling](#build-tooling)

## defineWidget

The entry point for every widget: a manifest (metadata), an optional Zod config schema, an optional `migrate` function, and a SolidJS component.

```ts
function defineWidget<C = Record<string, unknown>>(
  definition: WidgetDefinition<C>
): WidgetDefinition<C>
```

`WidgetDefinition<C>` shape:

```ts
interface WidgetDefinition<C = Record<string, unknown>> {
  manifest: WidgetManifest;
  configSchema?: ZodType<C, unknown>;
  migrate?: (config: Record<string, unknown>, fromConfigVersion: number) => Record<string, unknown>;
  component: (props: { config: C }) => any;
}
```

Providing `configSchema` auto-populates `manifest.schema` (JSON Schema) and `manifest.defaultConfig`. Types derive via `z.infer`; the dashboard renders the edit form from the schema.

## Manifest fields

The runtime loads the inline `manifest` in `defineWidget`; the CLI reads `src/<name>/manifest.json` for build and publish metadata (`bun widget upgrade` writes `sdkVersion` back to that file). Keep the two in sync: the build step warns on schema drift.

| Field           | Type                | Required  | Description                                                                                                                                                                                                                                                                                                  |
| --------------- | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`          | `string`            | yes       | Display name and identifier                                                                                                                                                                                                                                                                                  |
| `description`   | `string`            | no        | Short description shown in the widget picker                                                                                                                                                                                                                                                                 |
| `icon`          | `string`            | no        | Iconify icon, e.g. `"mdi:lightbulb"`                                                                                                                                                                                                                                                                         |
| `minSize`       | `{w, h}`            | yes       | Minimum grid size                                                                                                                                                                                                                                                                                            |
| `maxSize`       | `{w, h}`            | yes       | Maximum grid size. Dashboards are 4, 8, 12 or 16 columns wide (phone to wide screen), so a widget meant to span the whole width sets `w: 16`                                                                                                                                                                 |
| `defaultSize`   | `{w, h}`            | no        | Initial size when added to a dashboard                                                                                                                                                                                                                                                                       |
| `sdkVersion`    | `string`            | yes       | Required SDK version range, e.g. `"^1.0.0"`                                                                                                                                                                                                                                                                  |
| `capabilities`  | `CapabilityGrant[]` | yes (1.x) | Home Assistant domains the widget reads or controls; approved by the user; service calls are enforced by the host, reads are shown at install and checked at runtime in a later release. Empty array for widgets that touch no HA data. See [Capabilities & Permissions](/docs/widgets/widget-capabilities). |
| `configVersion` | `number`            | no        | Bump when config shape changes; used with `migrate`                                                                                                                                                                                                                                                          |
| `schema`        | `object`            | no        | Auto-populated from `configSchema`. Do not set manually.                                                                                                                                                                                                                                                     |
| `defaultConfig` | `object`            | no        | Auto-populated from Zod `.default()` calls.                                                                                                                                                                                                                                                                  |
| `cssUrl`        | `string`            | no        | Set by the build when the widget emits a stylesheet. Do not set manually.                                                                                                                                                                                                                                    |

## widgetFields

> **Deprecated in 1.4.0:** `widgetFields.*` and building `configSchema` with raw `z.object({ ... })` still
> work, but they're deprecated and will be removed in a future major release. Use
> the [Config API](/docs/widgets/widget-sdk/config) (`defineConfig` + `field.*`) instead, or
> run `bun widget migrate config`. `bun widget build` warns on each use.

Pre-built Zod field helpers; each attaches metadata so the dashboard renders the right form control.

| Helper                              | Renders as                                                         | Replacement              |
| ----------------------------------- | ------------------------------------------------------------------ | ------------------------ |
| `widgetFields.title()`              | Text input labelled "Title", hint "Optional display name override" | `field.title()`          |
| `widgetFields.entityIds(domain)`    | Multi-select entity picker filtered by domain                      | `field.entities(domain)` |
| `widgetFields.singleEntity(domain)` | Single-select entity picker filtered by domain                     | `field.entity(domain)`   |
| `widgetFields.areaId()`             | Area picker dropdown                                               | `field.area()`           |

See the [Config API](/docs/widgets/widget-sdk/config) for the full `field.*` surface (`field.text`, `field.number`, `field.toggle`, `field.choice`, `field.stringList`, `field.group`) and type inference with `Infer`.

## Complete widget example

A working example mirroring the official `@glasshome` widgets: config schema, entities bound from config via `useEntities`, group aggregation via `useWidgetEntityGroup`, a tap-to-toggle gesture, and a schema-driven settings dialog. Note that **you bind entities yourself** from the ids the user picked in config; there is no hidden injection.

```tsx
// src/my-lights/index.tsx
// Dialog chrome is injected into WidgetDialog as props, and the SDK re-exports it
// from @glasshome/ui so the imports stay gated by sdkVersion.
import {
  Button,
  defineConfig,
  defineWidget,
  field,
  Icon,
  type Infer,
  type LightGroupResult,
  ResponsiveDialog,
  ResponsiveDialogContent,
  ResponsiveDialogDescription,
  ResponsiveDialogHeader,
  ResponsiveDialogTitle,
  SchemaForm,
  Widget,
  WidgetDialog,
  useEntities,
  useService,
  useWidgetContext,
  useWidgetDialog,
  useWidgetEntityGroup,
  useWidgetGestures,
} from "@glasshome/widget-sdk";
import { onCleanup } from "solid-js";

const configSchema = defineConfig({
  title: field.title(),
  entityIds: field.entities("light"),
});

type Config = Infer<typeof configSchema>;

function MyLightsWidget(props: { config: Config }) {
  const ctx = useWidgetContext();
  const { setShowDialog, dialogProps } = useWidgetDialog();
  const { toggle } = useService();

  // Bind the entities the user picked in config. Reactive: updates when state changes.
  const entities = useEntities(() => props.config.entityIds);

  const { aggregatedData, emptyState, hasEntities } = useWidgetEntityGroup({
    entities,
    aggregationMode: () => "light",
    emptyStateConfig: {
      icon: <Icon icon="mdi:lightbulb-group" width={32} />,
      title: "No lights",
      message: "Hold to configure",
    },
  });

  // aggregatedData is typed for every mode; narrow it to the one you asked for.
  const light = () => aggregatedData() as LightGroupResult | undefined;
  const isOn = () => light()?.isOn ?? false;
  const onCount = () => light()?.onCount ?? 0;

  const gestures = useWidgetGestures(() => ({
    tap: () => toggle(entities().map((e) => e.id)),
  }));
  onCleanup(gestures.dispose);

  return (
    <>
      <Widget
        gestures={gestures}
        variant="classic-glass"
        tone={isOn() ? "accent" : "neutral"}
        emptyState={emptyState()}
        loading={!hasEntities()}
      >
        <Widget.Content>
          <Widget.Glyph icon="mdi:lightbulb-group" />
          <Widget.Head
            icon="mdi:lightbulb-group"
            name={props.config.title || "Lights"}
            active={isOn()}
            count={entities().length}
          />
          <Widget.Hero value={isOn() ? `${onCount()} on` : "Off"} />
        </Widget.Content>
      </Widget>
      <WidgetDialog
        {...dialogProps}
        ResponsiveDialog={ResponsiveDialog}
        ResponsiveDialogContent={ResponsiveDialogContent}
        ResponsiveDialogHeader={ResponsiveDialogHeader}
        ResponsiveDialogTitle={ResponsiveDialogTitle}
        ResponsiveDialogDescription={ResponsiveDialogDescription}
        Button={Button}
        SchemaForm={SchemaForm}
        title="Lights"
        maxWidth="lg"
        configSchema={configSchema}
        config={props.config}
        onConfigSave={(config) => {
          ctx.updateConfig(config);
          setShowDialog(false);
        }}
      />
    </>
  );
}

export default defineWidget<Config>({
  manifest: {
    name: "My Lights",
    description: "Toggle a group of lights",
    icon: "mdi:lightbulb-group",
    minSize: { w: 1, h: 1 },
    maxSize: { w: 8, h: 8 },
    sdkVersion: "^1.17.0",
    capabilities: [{ domain: "light", access: "control" }],
  },
  configSchema,
  component: MyLightsWidget,
});
```

> **Binding entities from config:** `useEntities(() => props.config.entityIds)` resolves the ids the user picked (via the `field.entities` picker) into live `EntityView[]`. Pass that accessor straight into `useWidgetEntityGroup`. A single-entity widget uses `field.entity(domain)`, which also stores `string[]`, so read the first id: `useEntity(() => props.config.entityIds[0] ?? "")`. The scaffold template ships this exact wiring.

> **The dialog needs UI components:** `<WidgetDialog>` renders no chrome of its own. It takes `ResponsiveDialog`, `Button`, `SchemaForm`, and friends as props so your widget controls the look. Import them from @glasshome/widget-sdk, which re-exports them behind `sdkVersion`; keep `@glasshome/ui` installed, since the build reads its styles. See [WidgetDialog component](#widgetdialog-component) for the full prop list.

## Widget component

`<Widget>` is the main container: context, gesture handling, theming, and base styling.

### Props

```ts
interface WidgetProps {
  variant?: string | WidgetVariantConfig;
  tone?: Tone;           // semantic color: resolves to --widget-color
  color?: string;        // CSS color override for --widget-color (overrides tone)
  colorTo?: string;      // second gradient stop (--widget-color-to)
  gradient?: string;     // full CSS gradient (overrides auto-shell)
  loading?: boolean;
  class?: string;
  isEditMode?: boolean;
  onDelete?: () => void;
  emptyState?: { icon?: JSX.Element; title?: string; message?: string };
  gestures?: GestureHandlers; // return value of useWidgetGestures(...)
  children?: JSX.Element;
}
```

Pass the return value of `useWidgetGestures(...)` to `gestures`, not a plain object.

### Variants

`variant` picks a pre-built look for the shell (padding, blur, background): a built-in id string or a `WidgetVariantConfig` object whose `styles` set the container class, inline style and CSS variables. The SDK ships three:

| `variant` id           | Look                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `"classic-glass"`      | Default glass: blurred translucent shell, roomy padding. What every official `@glasshome` widget uses.  |
| `"minimal"`            | No glass: transparent shell, tighter padding.                                                           |
| `"compact-horizontal"` | Lighter blur, tighter padding and a larger icon, for short, wide tiles. It does not change your layout. |

```tsx
<Widget variant="classic-glass" tone="accent">…</Widget>
```

> **No variant means no shell styling:** If you omit `variant`, no variant styling is applied (you get a bare shell). For the standard GlassHome look, pass `variant="classic-glass"`. The scaffold and every built-in widget do.

`WidgetVariantConfig` is exported from the SDK for fully custom looks. Only its `styles` field takes effect; `layout`, `elements`, `plugins`, `interactions` and `extends` are deprecated and removed in 2.0. Tone and color props ([Theming](#theming), [Styling](/docs/widgets/widget-styling#coloring-the-shell)) layer on top of whichever variant you choose.

### Slot subcomponents

| Slot                | Key props                                                | Purpose                                       |
| ------------------- | -------------------------------------------------------- | --------------------------------------------- |
| `Widget.Icon`       | `icon: JSX.Element`, `color?`, `dimmed?`, `entityCount?` | Entity icon with adaptive color               |
| `Widget.Title`      | `children`, `badge?`                                     | Primary label                                 |
| `Widget.Status`     | `children`, `dimmed?`, `isUnavailable?`                  | State text (On, Off, etc.)                    |
| `Widget.Value`      | `value: number \| string`, `unit?`, `interpret?`         | Numeric or formatted value. Takes no children |
| `Widget.Content`    | `children`, `class?`                                     | Freeform content area                         |
| `Widget.SliderFill` | `value: number` (0–100), `color?`, `isDragging?`         | Background fill for slider widgets            |

`Widget.SliderFill` is also exported standalone as `WidgetSliderFill` (same component). Pass `isDragging` so the fill tracks the finger 1:1 while dragging (drops the transition). See [Animation](/docs/widgets/widget-styling#animation).

### Component props

Every widget component receives one prop, `{ config: C }`, where `C` is the type passed to `defineWidget<C>`. The host parses, migrates, and validates the saved config first, so `config` always matches your current schema type.

## Hooks

### `useWidgetContext()`

Returns `ReactiveWidgetContext`. Must be called inside the `<Widget>` render tree.

```ts
interface ReactiveWidgetContext {
  isEditMode: () => boolean;
  updateConfig: (config: Record<string, unknown>) => void;
  dimensions: () => { width: number; height: number }; // CSS px, (0,0) before first layout
  registerDialogOpener?: (open: ((tab?: string) => void) | null) => void;
}
```

### `useWidgetEntityGroup(options)`

Aggregates one or many entities with built-in aggregation presets and empty-state support.

```ts
function useWidgetEntityGroup<TData = unknown>(
  options: UseWidgetEntityGroupOptions<TData>
): UseWidgetEntityGroupResult<TData>
```

**Options:**

| Field                | Type                                       | Description                                                                            |
| -------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- |
| `entities`           | `Accessor<EntityView[]>`                   | Reactive entities array (required)                                                     |
| `emptyStateConfig`   | `WidgetEmptyStateConfig`                   | Shown when `hasEntities` is false (required)                                           |
| `aggregationMode`    | `Accessor<AggregationPreset \| undefined>` | `"light"`, `"sensor"`, `"switch"`, `"binary-sensor"`, `"none"`                         |
| `sensorGroupType`    | `Accessor<SensorGroupType>`                | For `"sensor"` preset: `"min"`, `"max"`, `"mean"`, `"median"`, `"sum"`, `"last"`, etc. |
| `calculateGroupData` | `(entities: EntityView[]) => TData`        | Custom aggregation function (alternative to `aggregationMode`)                         |
| `allEntitiesMode`    | `boolean`                                  | All entities must be on for group to be "on" (light/switch presets)                    |
| `minEntities`        | `number`                                   | Minimum entities required; default `1`                                                 |

**Return value:**

| Field            | Type                                                           | Description                               |
| ---------------- | -------------------------------------------------------------- | ----------------------------------------- |
| `entities`       | `Accessor<EntityView[]>`                                       | All entities                              |
| `groupData`      | `Accessor<TData \| null>`                                      | Result of `calculateGroupData`, or `null` |
| `aggregatedData` | `Accessor<LightGroupResult \| SensorGroupResult \| undefined>` | Result of preset aggregation              |
| `emptyState`     | `Accessor<WidgetEmptyStateConfig \| undefined>`                | Defined when entities are missing         |
| `hasEntities`    | `Accessor<boolean>`                                            | Whether minimum entities are present      |
| `count`          | `Accessor<number>`                                             | Entity count                              |

`LightGroupResult` includes: `isOn`, `isUnavailable`, `brightness` (0-255), `brightnessPercent` (0-100), `color`, `onCount`, `totalCount`, `description`.

### `useWidgetDialog(defaultTab?)`

Controls the widget settings dialog. `defaultTab` defaults to `"controls"`.

```ts
interface WidgetDialogReturn {
  showDialog: () => boolean;
  setShowDialog: (open: boolean) => void;
  openDialog: (tab?: string) => void;
  closeDialog: () => void;
  activeTab: () => string;
  setActiveTab: (tab: string) => void;
  dialogProps: {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    activeTab: string;
    onActiveTabChange: (tab: string) => void;
  };
}
```

Spread `dialogProps` onto `<WidgetDialog>` to wire open/close and tab state automatically.

## WidgetDialog component

`<WidgetDialog>` is the settings/detail dialog: a tabbed panel that can host device controls, a schema-driven config editor, and a debug view. It renders **no chrome of its own**; you inject the dialog/button/form components as props.

### Injected UI components (required)

These have no defaults; the dialog will not render without them. Import them from `@glasshome/widget-sdk`, which re-exports them behind `sdkVersion`:

| Prop                          | Component                                                                                  |
| ----------------------------- | ------------------------------------------------------------------------------------------ |
| `ResponsiveDialog`            | Dialog/drawer wrapper                                                                      |
| `ResponsiveDialogContent`     | Body                                                                                       |
| `ResponsiveDialogHeader`      | Header region                                                                              |
| `ResponsiveDialogTitle`       | Title                                                                                      |
| `ResponsiveDialogDescription` | Subtitle/description                                                                       |
| `Button`                      | Buttons (save, delete, tab controls)                                                       |
| `SchemaForm`                  | Form renderer for schema-driven config editing. Required only when you use `configSchema`. |

> **Info:** The official `@glasshome` widgets bundle these into a single `widgetDialogProps` object and spread it (`<WidgetDialog {...widgetDialogProps} {...dialogProps} … />`). Do the same in your project to keep call sites short.

### Content props

| Prop                                             | Type                                                      | Purpose                                                                                                                                                             |
| ------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`                                          | `string`                                                  | Dialog title (required)                                                                                                                                             |
| `sheet`                                          | `() => JSX.Element`                                       | What holding the tile opens: only what the tile cannot show. A render function, built on open (SDK 1.17 and up). See [Tiles & Sheets](/docs/widgets/widget-sheets). |
| `tile`                                           | `{ icon?, tone?, backdrop? }`                             | Overrides for the sheet's look; it already wears the widget's icon and colour, so usually only `backdrop` (SDK 1.17 and up)                                         |
| `controlsContent`                                | `JSX.Element`                                             | Device-controls tab (sliders, buttons for the bound entities)                                                                                                       |
| `configSchema`                                   | `ZodType`                                                 | Enables the schema-driven config editor tab. Pair with `config` + `onConfigSave`.                                                                                   |
| `config`                                         | `Record<string, unknown>`                                 | Current config to seed the editor                                                                                                                                   |
| `onConfigSave`                                   | `(config) => void`                                        | Called with the new config; typically `ctx.updateConfig(config)` then close                                                                                         |
| `editContent`                                    | `JSX.Element`                                             | Custom config UI, used **instead of** the schema editor when `configSchema` is absent                                                                               |
| `debugContent`                                   | `JSX.Element`                                             | Debug tab body                                                                                                                                                      |
| `debugData`                                      | `string \| Record<string, unknown>`                       | Raw debug payload (rendered if no `debugContent`)                                                                                                                   |
| `tabs`                                           | `WidgetDialogTab[]`                                       | Fully custom tabs: `{ id, label, icon, content }[]`                                                                                                                 |
| `defaultTab` / `activeTab` + `onActiveTabChange` | `string` / controlled pair                                | Initial or controlled active tab                                                                                                                                    |
| `headerActions`                                  | `JSX.Element`                                             | Extra buttons in the header                                                                                                                                         |
| `onDelete`                                       | `() => void`                                              | Renders a delete action                                                                                                                                             |
| `maxWidth`                                       | `"sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "3xl" \| "4xl"` | Dialog max width                                                                                                                                                    |

Config-only widgets pass just `configSchema` + `config` + `onConfigSave`; control widgets add a `sheet` (SDK 1.17 and up), which also makes the tile hold to open it, or `controlsContent`. The tab row shows only with Developer Mode on. See the [complete example](#complete-widget-example) for the wiring.

### `useWidgetGestures(config, orientation?)`

Tap, hold, and slide gestures on any pointer type.

```ts
function useWidgetGestures(
  config: () => GestureConfig,
  orientation?: () => "horizontal" | "vertical" | "square"
): GestureHandlers
```

Both arguments are SolidJS accessors; orientation is optional and defaults to horizontal.

```ts
interface GestureConfig {
  tap?: () => void;
  hold?: { action: () => void; delay?: number }; // delay in ms, default 550
  slide?: {
    value: number;
    onChange: (value: number) => void;
    min?: number; max?: number;
    orientation?: "auto" | "horizontal" | "vertical";
    activationDelay?: number;
  };
}
```

`GestureHandlers` (returned value) carries `onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerCancel`, `onPointerEnter`, `bindElement`, `touchAction`, and `dispose`. Pass the whole object to `<Widget gestures={...} />`. Call `dispose()` in `onCleanup`.

### `useReducedMotion()`

Reactive `prefers-reduced-motion` accessor, `() => boolean`; `false` when `matchMedia` is unavailable (SSR-safe).

### `useIntersectionPause(elAccessor)`

Returns `() => boolean`: `true` while the element (passed as an accessor) is off-screen; `false` when `IntersectionObserver` is unavailable (SSR-safe).

## Home Assistant data & services

Import all Home Assistant hooks from `@glasshome/widget-sdk`, never from `@glasshome/sync-layer` directly (a direct import fails the build and would bundle a second, disconnected store). Read hooks are SolidJS accessors backed by the host's live store, so UI stays reactive with no WebSocket state to manage. Service calls are checked against your declared [capabilities](/docs/widgets/widget-capabilities); declare your reads too, since read checks are coming.

### Reading entities

| Hook                               | Returns                                       | Purpose                                                            |
| ---------------------------------- | --------------------------------------------- | ------------------------------------------------------------------ |
| `useEntity(id)`                    | `Accessor<EntityView \| undefined>`           | One entity by id (`id` may be a string or accessor)                |
| `useEntities(ids)`                 | `Accessor<EntityView[]>`                      | Many entities; pass an accessor returning the id array             |
| `useArea(id)`                      | `Accessor<AreaView \| undefined>`             | An area and its entities                                           |
| `useEntityHistory(id)`             | `Accessor<EntityHistoryData \| undefined>`    | Recent state history for an entity                                 |
| `useEntityStatistics(id, options)` | `Resource<StatisticValue[]>`                  | Long-term statistics; the resource exposes `.loading` and `.error` |
| `useForecast(id)`                  | `Accessor<WeatherForecastsData \| undefined>` | Weather forecast for a weather entity                              |
| `useCamera(id)`                    | `{ stream, refresh }`                         | Reactive camera stream data                                        |
| `useStore(selector)`               | `Accessor<T>`                                 | Escape hatch for direct store access when no specific hook fits    |

```tsx
import { useEntity, useEntities } from "@glasshome/widget-sdk";

function Thermostat(props: { config: Config }) {
  const climate = useEntity(() => props.config.entityIds[0] ?? "");
  const temp = () => climate()?.attributes.current_temperature;
  return <Widget.Value value={temp() ?? "—"} />;
}
```

### The `EntityView` shape

Every read hook returns `EntityView` objects. The most-used fields:

| Field                         | Type                  | Notes                                                                                                  |
| ----------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
| `id`                          | `string`              | Entity id, e.g. `"light.kitchen"`                                                                      |
| `domain`                      | `string`              | `"light"`, `"sensor"`, …                                                                               |
| `state`                       | `string`              | Current state value (`"on"`, `"22.5"`, …)                                                              |
| `attributes`                  | `Record<string, any>` | HA attributes, **minus** the four surfaced below. Read those from the canonical fields, not from here. |
| `friendlyName`                | `string`              | Display name                                                                                           |
| `icon`                        | `string \| null`      | Resolved icon (registry → attribute → domain default)                                                  |
| `deviceClass`                 | `string \| null`      | Resolved device class                                                                                  |
| `unitOfMeasurement`           | `string \| null`      | Resolved unit                                                                                          |
| `areaId` / `deviceId`         | `string \| null`      | Placement / device                                                                                     |
| `lastChanged` / `lastUpdated` | `Date`                | Timestamps                                                                                             |

`EntityView` is exported as a type from the SDK for your own annotations.

### Connection & locale

| Hook                   | Returns                             |
| ---------------------- | ----------------------------------- |
| `useConnection()`      | `{ status, isConnected }` accessors |
| `useHassConfig()`      | `Accessor<HassConfig \| null>`      |
| `useUnitSystem()`      | `Accessor<HassUnitSystem \| null>`  |
| `useTemperatureUnit()` | `Accessor<string>` (e.g. `"°C"`)    |
| `useLocale()`          | `Accessor<string>` (BCP 47)         |
| `useCurrency()`        | `Accessor<string>` (ISO 4217)       |

### Calling services

`useService()` returns a capability-routed service caller plus shortcuts that take an entity id (or array) and optional service data:

```tsx
import { useService } from "@glasshome/widget-sdk";

function LightToggle(props: { config: Config }) {
  const { toggle, turnOn, callService } = useService();

  return (
    <Widget gestures={useWidgetGestures(() => ({ tap: () => toggle(props.config.entityId) }))}>
      {/* turnOn("light.x", { brightness_pct: 80 }) */}
      {/* callService("light", "turn_on", { brightness_pct: 80 }, { entity_id: "light.x" }) */}
    </Widget>
  );
}
```

| Hook                                           | Returns                                    |
| ---------------------------------------------- | ------------------------------------------ |
| `useService()`                                 | `{ callService, turnOn, turnOff, toggle }` |
| `useTurnOn()` / `useTurnOff()` / `useToggle()` | The single shortcut function               |

`callService(domain, service, data?, target?)` is the general form; it resolves to `Promise<void>`. The host validates the call against your granted capabilities before forwarding it.

## Errors and empty states

- **Config schema parse failure.** If the stored config fails Zod validation after migration, the dashboard falls back to `configSchema.parse({})` (all-defaults), then to `{}`. No error is shown to the user; handle missing/default config gracefully.
- **Missing entities.** Pass an `emptyStateConfig` to `useWidgetEntityGroup`. When `hasEntities()` is false, `emptyState()` returns the config object; pass it to `<Widget emptyState={emptyState()}>` to render the built-in empty state UI.
- **Component throw.** A render throw is caught by an `ErrorBoundary`: after one crash, a retryable error card with a Retry button; after three crashes (configurable in the Dash host), a permanent "disabled" card until the page is refreshed.

## Theming

Widgets inherit the dashboard theme automatically and render in an isolated shadow root with their own CSS bundle. The essentials:

- Use the `tone` prop on `<Widget>` for semantic color (`tone="accent"`), or `color`/`colorTo`/`gradient` for a custom shell.
- `isDark()` returns the current theme as a boolean for logic; `dark:` Tailwind variants work in markup.
- The SDK also exports `injectTokens`, `Tone`, and `ToneSchema`.

The full styling model (Tailwind, your own CSS, theme variables, container queries, animation): **[Styling & Animation](/docs/widgets/widget-styling)**.

## Entity utilities

| Function                                                        | Description                                           |
| --------------------------------------------------------------- | ----------------------------------------------------- |
| `isEntityActive(entity)`                                        | `true` for active states                              |
| `getEntityAttribute(entity, key)`                               | Read a specific attribute                             |
| `countActiveEntities(entities)`                                 | Count of currently active entities                    |
| `calculateLightGroup(entities, allEntitiesMode?)`               | Aggregate brightness/color/state across a light group |
| `calculateSensorGroup(entities, groupType?, ignoreNonNumeric?)` | Aggregate numeric sensors                             |

## Build tooling

The scaffolded `vite.config.ts` wires this up automatically. Two entrypoints for custom setups:

### `@glasshome/widget-sdk/vite`

| Export                             | Purpose                                                          |
| ---------------------------------- | ---------------------------------------------------------------- |
| `glasshomeWidget(options?)`        | Single-widget plugin: dev preview + library build                |
| `glasshomeWidgets(options?)`       | Multi-widget project plugin: per-widget builds + `registry.json` |
| `buildWidgets(options?)`           | Programmatic build of all widgets (used by `bun widget build`)   |
| `discoverWidgets(srcDir)`          | Scan `src/` for subdirs containing `index.tsx` + `manifest.json` |
| `generateRegistry(srcDir, outDir)` | Write a `registry.json` from discovered manifests                |
| `isWidgetExternal(id)`             | `true` for host-provided packages (not bundled)                  |

### `@glasshome/widget-sdk/schemas`

Zod schemas for validating manifests and publish payloads. Used by the CLI and Hub.

Re-exported from the canonical [`@glasshome/widget-contract`](/docs/widgets/widget-capabilities) package shared by the SDK, CLI, Hub, and Dash, so the same schema validates a manifest everywhere.

| Export                                                                | Purpose                                           |
| --------------------------------------------------------------------- | ------------------------------------------------- |
| `widgetManifestSchema` (alias `WidgetManifestSchema`)                 | Validate a complete widget manifest               |
| `publishManifestSchema`                                               | Validate the manifest subset sent at publish time |
| `capabilitiesSchema`                                                  | Validate a `capabilities` array                   |
| `capabilityGrantSchema`                                               | Validate a single `{ domain, access }` grant      |
| `GridSizeSchema`                                                      | Validate `{ w, h }`                               |
| `parseGridSize` / `serializeGridSize`                                 | Parse from / serialize to the stored JSON form    |
| `formatSchemaError`                                                   | Turn a Zod error into a readable message          |
| `PublishRequestSchema` / `PublishConfirmSchema` / `PublishBodySchema` | Publish-flow request bodies                       |
| `CapabilityGrant` (type)                                              | TypeScript type for one grant                     |

`formatSchemaError` and `WidgetManifestSchema` are also available from the SDK main entry (`@glasshome/widget-sdk`) for convenience.