# GlassHome (full docs) > Concatenated, machine-readable bundle of every GlassHome doc page. One fetch. Frontmatter preserved per page so you can split by `---`. Generated: 2026-09-06T21:14:11.738Z Source site: https://glasshome.app/docs --- title: "Glossary" description: "A-Z definitions of GlassHome terminology. Quick lookup for terms used across the docs." slug: glossary canonical: https://glasshome.app/docs/glossary section: "Reference" updated: 2026-06-09 --- # Glossary ## A **Account dashboard.** The page at [glasshome.app/dashboard](/dashboard) where you manage your Hub account, view your Pro license card, and see your member number. **Addon.** A packaging of GlassHome Dash for Home Assistant OS. Installed via the HA Add-on Store. See [Addon](/docs/dash/addon). **Area.** Home Assistant's grouping of devices/entities by room. Some widgets bind to an area instead of a single entity. ## B **Breakpoint.** Layout slot keyed by screen width. `lg` (>=1024px), `md` (>=768px), `sm` (\<768px). Each dashboard stores one layout per breakpoint. See [Layouts](/docs/dash/layouts). **Bundle.** Compiled JS file produced by `bun widget build`, one per widget, plus a `registry.json`. ## C **Config dialog.** The settings panel that opens when you tap the gear or edit affordance on a widget in edit mode. Fields are auto-generated from the widget's Zod schema. Changes save instantly. **configVersion.** Integer on a widget manifest. Incremented on breaking config changes. Drives [migrations](/docs/widgets/widget-migrations). ## D **Dash.** The local dashboard app (the `glasshome-dash` container) that runs on your hardware and connects directly to Home Assistant. This is the GlassHome UI you interact with day to day. **Dashboard.** A named, individually-laid-out screen inside Dash. Multiple dashboards live side by side and are switched via the dock at the bottom. Each has its own widget set and independent layout per breakpoint. **defineWidget.** SDK entry point. Wraps a manifest, optional Zod schema, and SolidJS component into a registerable widget. See [Widget SDK](/docs/widgets/widget-sdk). **Demo Mode.** A simulated HA backend baked into the dashboard. Lets you evaluate GlassHome without a real HA instance. See [Connecting](/docs/dash/connecting#connection-methods). **Detail dialog.** The overlay that opens when you long-press (500 ms) a widget in live mode. Hosts fine controls (sliders, advanced actions) that don't fit on the tile itself. ## E **Early Bird.** The introductory Pro offer that ran during the beta. It has ended, and Pro is now at its standing price. Early Bird buyers keep a gold license card. See [Pro](/docs/hub/pro). **Edit mode.** The state Dash enters when you tap the pencil icon in the header. A faint grid overlay appears, widgets show resize grips and X buttons, and gestures shift from device control to layout editing. See [Editing](/docs/dash/editing). **Entity.** A Home Assistant primitive (`light.kitchen`, `sensor.temp`). Widgets bind to one or more entities. ## H **Hub.** The GlassHome cloud service at glasshome.app. Hosts the widget registry, user accounts, and organizations. Never sees dashboard data. **HAOS.** Home Assistant Operating System. Includes Supervisor and addon support. ## L **License / member number.** Proof of a Pro purchase. Your sequential member number is assigned once at checkout and never changes. Shown on your [account dashboard](/dashboard). ## M **Manifest.** `manifest.json` (or inline metadata in `defineWidget`). Declares widget name, icon, size limits, SDK version, configVersion. **Migration.** A function on a widget that transforms old saved config into the current shape when `configVersion` increases. ## O **Organization.** A shared publishing scope (`@org-slug`). Multiple members publish under one namespace. See [Organizations](/docs/hub/organizations). ## P **Polar.** The payment platform GlassHome uses for Pro checkout. Handles billing and receipt emails. See [Pro → How to buy](/docs/hub/pro#how-to-buy). **Pro.** The one-time purchase that adds community widgets and theming (the theme editor, custom backgrounds, theme upload) to Dash. Not a subscription. See [Pro](/docs/hub/pro). ## R **Registry.** Hub's catalog of published widgets. The dashboard queries it during widget browse/install. Bundles served from `cdn.glasshome.app`. **Remote address.** A web address you reach Dash at from outside your home, published by your own VPN, reverse proxy, or tunnel, and declared in Dash settings. See [Remote Access](/docs/dash/remote-access). ## S **Scope.** Publishing namespace. Your personal scope is your [username](#u) (`@username`); an organization's scope is its slug (`@org-slug`). Widget IDs look like `@scope/widget-name`. `@glasshome` is the reserved official scope and cannot be claimed by users or organizations. **SDK.** `@glasshome/widget-sdk`. Provides `defineWidget`, `Widget` components, hooks, entity helpers, theming utilities. **SDK version.** Range string in a manifest (e.g. `^0.5.0`) declaring which SDK majors the widget supports. Dash rejects widgets built against an unsupported SDK. ## T **Theme preset.** One of the 7 named built-in themes (Midnight Glass, Sunrise Studio, Forest Zen, Lavender Dreams, Coral Reef, Monochrome Pro, Ocean Breeze). Each ships with light and dark variants. Switching presets is free; editing or uploading a custom theme requires Pro. See [Themes](/docs/dash/themes). **Trust badge.** `Official` (verified, installs without prompt) or `Community` (user-published, asks for consent). Shown on every widget in the picker. ## U **Username.** Your personal publishing handle, which is also your personal [scope](#s) (`@username`). It is auto-assigned at signup by slugifying your display name (lowercased, non-alphanumeric runs collapsed to dashes, e.g. "Alice Test" becomes `alice-test`), and is unique per account (a later collision gets a numbered suffix like `alice-test-2`). It is decoupled from your display name: renaming your display name does not change your username. You can change your username in your [account dashboard](/dashboard) settings, but not while you have widgets published under your current scope. See [Publishing Widgets](/docs/widgets/widget-publishing#publishing-scopes). ## W **Widget.** A tile on a dashboard bound to one or more entities. SolidJS component + Zod config. **Widget CLI.** `@glasshome/widget-cli`. Scaffolds, builds, validates, and publishes widgets. See [Widget CLI](/docs/widgets/widget-cli). **WebSocket (WS).** The protocol Dash uses to talk to Home Assistant. Direct, low-latency, no cloud relay. --- title: "FAQ" description: "Frequently asked questions about the Dash app. Home Assistant, offline use, hardware, addon behavior, and remote access." slug: dash/faq canonical: https://glasshome.app/docs/dash/faq section: "Support" updated: 2026-08-10 --- # FAQ Short answers about the Dash app. Each links to deeper docs. Questions about accounts, pricing, and Pro live in the [Hub FAQ](/docs/hub/faq); widget development questions in the [Widget Dev FAQ](/docs/widgets/faq). ## Does GlassHome replace Home Assistant? No. Home Assistant still runs your automations and integrations. GlassHome is a UI in front of it. See [Concepts](/docs/dash/concepts). ## Does it work without internet? Yes. Dash and HA talk on your LAN, and your dashboard loads at full speed with the internet unplugged: every widget you have installed is already stored on your box. Internet is only required to install a widget for the first time, to check for widget updates, or to reach the dashboard from [outside your home](/docs/dash/remote-access). Those catch up on their own once you are back online. ## What hardware do I need? Any 64-bit CPU with SSE4.2 (Intel 2008+, AMD 2011+) or ARMv8 (Pi 4/5, Apple Silicon). Pi 3 is too slow. VMs need a modern CPU profile, see [Troubleshooting](/docs/dash/troubleshooting#illegal-instruction). ## Can I run it on the same machine as Home Assistant? Yes. Most users do. Just make sure ports don't collide (HA is `8123`, GlassHome is `3123` by default). ## Does the addon show up in the Home Assistant sidebar (ingress)? No. GlassHome runs as its own web app on port `3123`, not through HA ingress. After starting the addon, open `http://:3123` in a browser. This keeps the dashboard full-screen and independent of the HA frontend. See the [Addon guide](/docs/dash/addon). ## How do I access my dashboard away from home? A VPN (WireGuard, Tailscale), or your own address behind a reverse proxy or tunnel. See [Remote Access](/docs/dash/remote-access) for the comparison and the setup steps. ## Does it support YAML config? No. Everything is configured in the UI and stored in SQLite. That's intentional; see [What GlassHome is not](/docs/dash). ## Can I sync dashboards between devices? Every device pointed at the same Dash instance sees the same dashboards. There's no cloud sync of layouts between separate Dash installs. ## Where do I report a bug or ask for a feature? [Discord](https://discord.gg/FJYdeDmrzv) for chat and bug reports. Template in [Bug Reports](/docs/dash/bug-reports). --- title: "Introduction" description: "GlassHome is a modern, local-first dashboard for Home Assistant. Install in minutes, customize without YAML." slug: dash canonical: https://glasshome.app/docs/dash section: "Getting started" updated: 2026-06-09 --- # Dash Documentation A modern dashboard for Home Assistant. Connects directly to your HA instance over WebSocket. No cloud relay, no telemetry, no YAML required. ![GlassHome dashboard overview](/assets/screenshots/home-midnight-glass-light-desktop.webp) ## Start here ### [Quickstart](/docs/dash/quickstart) Install, connect, and add your first widget in 5 minutes. ### [Concepts](/docs/dash/concepts) Hub, dashboard, addon, widget. The mental model in one page. ### [Installation](/docs/dash/installation) Pick a deployment method: HA addon, Docker, or container. Building widgets instead of using them? That's its own docs space: [Widget Dev](/docs/widgets/widget-development). ## Common tasks - [Connect to Home Assistant](/docs/dash/connecting): OAuth flow, demo mode, URL gotchas. - [Edit a dashboard](/docs/dash/editing): add, move, resize, remove. Desktop and touch gestures. - [Access from anywhere](/docs/dash/remote-access): VPN or your own address. - [Theme it](/docs/dash/themes): 7 presets or custom palette, light and dark. - [Something broke](/docs/dash/troubleshooting): look up by error message or symptom. [Give people and screens their own access](/docs/dash/people-and-access): rooms, controls, dashboards, invite links, shared tablets. ## What GlassHome is not - **Not a replacement for Home Assistant.** HA still runs your automations, integrations, and devices. GlassHome is the UI layer in front of it. - **Not a cloud service.** Dash runs on your hardware. Your Hub account exists only to publish widgets and hold your license. - **Not config-as-code.** Everything is configured in the UI. No YAML, no restart-to-apply. > **Need help?:** Join the Discord for live support, or file a [bug report](/docs/dash/bug-reports). --- title: "Installation" description: "Install GlassHome as a Home Assistant addon, Docker Compose service, or standalone container. CPU requirements and VM gotchas covered." slug: dash/installation canonical: https://glasshome.app/docs/dash/installation section: "Installation" updated: 2026-06-09 --- # Installation All install methods deploy the same app. Pick by environment, not by feature set. ## Pick a method | Your setup | Use | | ------------------------------- | ------------------------------------------------------ | | Home Assistant OS or Supervised | [HA Addon](/docs/dash/addon) (recommended) | | HA Container / Core | [Docker Compose](/docs/dash/docker#docker-compose) | | Any other machine with Docker | [Direct Container](/docs/dash/docker#direct-container) | | Bare metal without Docker | Not supported. Run Docker or use HA OS. | ## Requirements - **A running Home Assistant.** Any install method, reachable on your network. Any recent Home Assistant release works. - **Docker** (unless using the HA addon), or **HAOS/Supervised with Supervisor** for the addon. - **Modern browser** to use Dash: Chrome, Firefox, Safari, or Edge. ## Supported CPUs GlassHome ships multi-arch images. Docker picks the right build automatically. ### x86-64 (amd64) Requires SSE4.2. Intel Nehalem (2008+) or AMD Bulldozer (2011+) and newer. Almost any PC from the last decade. ### ARM64 (aarch64) Raspberry Pi 4 and 5, Apple Silicon, most modern ARMv8 SBCs. Pi 3 and earlier are not supported. > **Running in a VM? Read this.:** Many hypervisors expose a Pentium 4-era CPU profile by default that hides SSE4.2 from the guest, even when the host CPU is modern. GlassHome will crash with `signal 4 / illegal instruction`. Switch the guest CPU to `host` (Proxmox), disable EVC (ESXi), or turn off processor compatibility mode (Hyper-V). Full fix: [Troubleshooting → Illegal Instruction](/docs/dash/troubleshooting#illegal-instruction). ## After installing 1. Open `http://:3123`. 2. Run the setup wizard. See [Connecting to Home Assistant](/docs/dash/connecting). 3. Add your first widget. See [Quickstart](/docs/dash/quickstart). ## Uninstalling - **Addon:** Uninstall from the Add-on Store. Optionally check "remove data". - **Docker:** `docker rm -f glasshome-dash && docker volume rm glasshome_data` (this deletes your dashboards). --- title: "Troubleshooting" description: "Look up GlassHome errors and fixes by symptom or message. Illegal instruction, container won't start, CORS, connection drops, and more." slug: dash/troubleshooting canonical: https://glasshome.app/docs/dash/troubleshooting section: "Troubleshooting" updated: 2026-08-10 --- # Troubleshooting Find your symptom, follow the fix. If nothing matches, file a [bug report](/docs/dash/bug-reports). ## Quick index - [Container or addon crashes on start with `signal 4` / illegal instruction](#illegal-instruction) - [Setup wizard rejects the HA URL](#bad-ha-url) - [Connected, but entities never load](#no-entities) - [Dashboard loads but blank / errors in browser console](#blank-ui) - [Can't reach the dashboard from outside home](#remote) - [Widget install fails / shows "incompatible SDK"](#widget-sdk) - [Layout / theme reset after update](#data-loss) - [Widget shows "Unavailable"](#widget-unavailable) - [HA login loop / OAuth fails](#oauth-loop) - [Theme or background not applying](#theme-not-applying) --- Container crashes with "Illegal Instruction" (signal 4) ### Symptoms Container or addon dies instantly. Logs show `signal 4`, `SIGILL`, `trap invalid opcode`, or `Illegal instruction`. ### Cause CPU does not expose SSE4.2. GlassHome requires Intel Nehalem (2008+) or AMD Bulldozer (2011+). On modern hardware, the actual cause is almost always a hypervisor hiding instructions from the guest. ### Fix: virtualized host By default many hypervisors expose a Pentium 4 era CPU profile for compatibility. Switch the guest CPU to one that exposes the host's real features. **Proxmox** 1. Shut down the VM running HA / GlassHome. 2. Hardware > Processor > Type: change `kvm64` to `host`. 3. Start the VM. In a Proxmox cluster: use `x86-64-v3` instead of `host` to keep live-migration working between v3-capable nodes. **VMware ESXi / Workstation.** Disable EVC, or raise the EVC baseline to one that includes SSE4.2. **Hyper-V.** Disable processor compatibility mode on the VM. **VirtualBox.** Enable Nested VT-x/AMD-V. Avoid legacy CPU profiles. ### Fix: bare metal If GlassHome runs directly on hardware (no hypervisor), the CPU itself is below the minimum. No software fix. Move to a Raspberry Pi 4/5 or any PC from the last decade. --- Setup wizard rejects the HA URL The URL must include protocol and the port HA actually serves on. Verify by opening it in a browser tab. If you can't reach HA in your browser, the dashboard can't either. Common mistakes: - Missing `http://` or `https://`. - Wrong port (HA's UI port, not the API). - HTTPS on dashboard side but plain HTTP on HA (or vice versa). They must match. - `localhost` from inside a container points at the container, not your machine. Use the LAN IP or `host.docker.internal`. See [Connecting → Entering the URL](/docs/dash/connecting#entering-the-url). --- Connected, but no entities load - The HA user you authorized as may have no entity permissions. Re-authorize with an admin user. - HA's `auth` integration may be using a custom provider that strips refresh tokens. Confirm HA's `configuration.yaml` doesn't disable refresh tokens. - If entities load partially, you've likely hit an HA WS rate limit during initial load. Refresh. --- Dashboard loads blank Open browser devtools (F12) > Console. Look for red errors: - **`Failed to fetch dynamically imported module`**: a widget bundle 404'd. Likely an installed community widget was unpublished. Remove the widget from your layout. - **`SecurityError: Failed to construct WebSocket`**: HTTPS dashboard trying to reach HTTP HA, or vice versa. Match protocols. - **`Refused to connect`**: CSP violation. Usually means you're proxying the dashboard and the proxy strips required headers. Bypass the proxy to confirm. --- Can't reach the dashboard from outside home - **Page loads, then stays empty**: your proxy is not forwarding WebSocket upgrades. Pass `Upgrade` and `Connection` through. - **Sign-in is refused**: the address isn't declared. Add it under **Settings > Remote access** (household admin), on the local address if you're locked out. - **"Public addresses need GlassHome Pro"**: the hostname resolves to a public IP. Addresses that resolve to a home IP stay free. - **Home Assistant sign-in fails while away**: expected. Use your GlassHome account or Quick connect. More: [Remote Access](/docs/dash/remote-access). --- Widget install fails with "incompatible SDK" The widget was built against a newer `@glasshome/widget-sdk` than the dashboard supports. Update the dashboard (`docker compose pull && docker compose up -d`, or addon update). If you are the widget's author, see [Widget Dev → Troubleshooting](/docs/widgets/troubleshooting#incompatible-sdk). --- Dashboards or theme disappeared after update Data lives in the Docker volume `glasshome_data` (mounted at `/data`). If it was missing on restart, the dashboard initializes a fresh DB. - Confirm the volume is mounted: `docker inspect glasshome | grep Mounts -A 10`. - If you migrated hosts, copy `/data` from the old host before first start on the new one. - HA addon stores data in addon-managed storage. Re-installing the addon with "remove data" unchecked preserves dashboards. If your data really is gone, file a [bug report](/docs/dash/bug-reports) with logs from the failing start. --- Widget shows "Unavailable" The entity the widget is bound to is missing or has been renamed in Home Assistant. - Check the entity still exists in HA (Settings > Devices & Services > Entities). - If it was renamed or removed, open the widget's config dialog in edit mode and rebind it to the correct entity. --- HA login loop / OAuth fails If you keep being redirected back to the HA login page or authorization never completes: - Confirm the HA URL you entered is reachable from the browser (not just from the server). Open it in a new tab. - Check that the URL uses the correct protocol (`http://` vs `https://`). A mismatch causes silent CORS failures. - Make sure popups are not blocked. The OAuth flow opens in a popup or redirect depending on the browser. See [Connecting to Home Assistant](/docs/dash/connecting) for URL format requirements and common pitfalls. --- Theme or background not applying - Hard-refresh the page (Ctrl+Shift+R / Cmd+Shift+R) to clear any cached assets. - Confirm you are applying the change in **Settings > Theme** and that the save completes (changes persist immediately; there is no separate save button). - If a custom theme or uploaded background is not showing, confirm the file format is supported and try re-uploading. Creating or editing custom themes and uploading backgrounds requires [Pro](/docs/hub/pro); applying an existing custom theme does not. - If the issue persists after a refresh, check the browser console for errors and file a [bug report](/docs/dash/bug-reports). --- title: "Widgets" description: "Widget types built into Dash, configuring widgets, community widgets from Hub, and building your own." slug: dash/widgets canonical: https://glasshome.app/docs/dash/widgets section: "Dashboards" updated: 2026-08-25 --- # Widgets A widget is a tile bound to one or more Home Assistant entities. Dash ships with widgets for lights, switches, sensors, climate, covers, scenes, and more. Community widgets are installed from Hub. ![Widget picker dialog](/assets/screenshots/widgets-midnight-glass-dark-desktop.webp) ## Adding a widget Adding widgets is part of edit mode. See [Editing](/docs/dash/editing) for the full step-by-step and gesture reference. ## Configuring a widget Open edit mode and tap the gear or edit affordance on any widget. The config dialog opens with fields auto-generated from the widget's schema: bound entity, display options, size, color, and more. Changes save instantly, no apply button. For the complete gesture reference (including how the config dialog relates to the detail dialog in live mode), see [Editing](/docs/dash/editing). ## Your own images Some widgets ask for a picture: a photo frame, a room header, a scene tile with your own artwork. Tap the image field in the widget's settings, pick one you already uploaded, or upload a new one from your phone or computer. The gallery belongs to the household, not to you. Everyone in the home sees every uploaded image and can delete any of them, so the picture your partner uploaded is one you can pick, and the one you uploaded is one they can remove. Each tile says whether it is in use, and how many widgets would lose their picture if it went. A household gets **250MB** and **500 images** in total. Uploads are converted to WebP, so most photos land far smaller than they started. When you hit either ceiling the upload is refused and the picker says so; delete something you are not using and try again. > **A deleted image can linger in caches:** Images are served with a one-year cache, which is what keeps your dashboard > instant. A browser that already loaded a picture can keep showing it for up to > a year after you delete it, and anyone who saved the link keeps it until the > image is gone from the server. Treat delete as "stop using this", not as "make > this unseeable". ## Built-in widget types All official `@glasshome` widgets are included in Dash and available to every user for free. | Widget | What it does | | ------------- | ------------------------------------------------------------- | | Light | Toggle, dim, color-temp. Supports groups and color lights. | | Switch | Toggle any switch or input boolean. | | Sensor | Display a sensor value with configurable unit and precision. | | Climate | Temperature setpoint, HVAC mode, fan mode. | | Fan | Speed, presets, oscillation, and direction. | | Cover | Open, close, and position blinds, garage doors, and shutters. | | Scene | Activate a scene with a single tap. | | Area | Area overview with entity grouping and batch controls. | | Batteries | Auto-discover and monitor battery levels across all devices. | | Binary Sensor | Motion, door, occupancy, and other binary sensors. | | Button | Press a button entity. | | Camera | Live camera stream with multi-protocol support. | | Locks | Lock and unlock with status display. | | Blinds | Position slider and tilt control. | ## Community widgets > **Pro required:** Installing community widgets requires a [Pro](/docs/hub/pro) license. Official `@glasshome` widgets are free for everyone. Browse and install community widgets from the widget picker (tap **Get more widgets** at the bottom), or browse the registry at [glasshome.app/#widgets](/#widgets). - **Official** widgets are verified by GlassHome and install without a prompt. - **Community** widgets ask for consent before install (third-party code). The bundle is downloaded once from Hub's CDN, then cached and served locally. ## Build your own Custom widgets are SolidJS components with a Zod config schema. Dash auto-generates the edit form from the schema, so you do not write any settings UI. Start with [Getting Started](/docs/widgets/widget-development). --- title: "Quickstart" description: "Install GlassHome, connect it to Home Assistant, and add your first widget in under 5 minutes." slug: dash/quickstart canonical: https://glasshome.app/docs/dash/quickstart section: "Getting started" updated: 2026-06-09 --- # Quickstart End-to-end in under 5 minutes: install, connect, customize. Assumes you already have Home Assistant running. ## Before you start - A reachable Home Assistant URL (e.g. `http://homeassistant.local:8123`). - A device to run GlassHome on. Same machine as HA is fine. CPU requirements: SSE4.2 / ARMv8 ([details](/docs/dash/installation#supported-cpus)). - No HA yet? You can still try demo mode. Skip to step 2 and pick **Demo mode** on the welcome screen. 1. **Install** Pick one. Both deploy the same app. **HA OS or Supervised users.** Add the [GlassHome Addon repository](/docs/dash/addon) and install from the Add-on Store. One-click, no Docker needed. **Everyone else.** Run the container: ```bash docker run -d \ --name glasshome-dash \ -p 3123:3123 \ -v glasshome_data:/data \ --restart unless-stopped \ ghcr.io/glasshome/dash:latest ``` 2. **Open Dash** Go to `http://:3123` in your browser. The setup wizard loads. For addon installs, `` is your Home Assistant machine's IP address (e.g. `http://192.168.1.100:3123`). No HA yet? Click **Demo mode** on the welcome screen to explore with simulated data. 3. **Connect to Home Assistant** Enter your HA URL (with `http://` or `https://` and the port). Dash sends you to the HA login page to authorize; it redirects back to Dash automatically. Stuck? See [Connecting](/docs/dash/connecting#troubleshooting). 4. **Add your first widget** 1. Tap the **pencil** icon to enter edit mode. 2. Tap **Add Widget**. 3. Pick a widget type, then pick the entity to bind. 4. Tap **Done**. Changes saved automatically. 5. **Make it yours** - **Settings > Theme.** Pick a preset or customize. [Themes guide](/docs/dash/themes). - **Settings > Dashboards.** Add more dashboards, each with its own layout. - **Settings > Remote Access.** Reach the dashboard from outside your home. [How](/docs/dash/remote-access). ## What next - **Use it well:** [Widgets](/docs/dash/widgets), [Editing](/docs/dash/editing), [Layouts](/docs/dash/layouts). - **Build your own widgets:** [Getting Started](/docs/widgets/widget-development). - **Something off:** [Troubleshooting](/docs/dash/troubleshooting) or post in [Discord](https://discord.gg/FJYdeDmrzv) and I'll take a look. > **Mental model first?:** If terms like Hub, addon, scope, and widget are blurring together, the [Concepts](/docs/dash/concepts) page is one screen. --- title: "Concepts" description: "The GlassHome mental model. Dash, Hub, addon, widget. What each piece is, where it runs, and how they connect." slug: dash/concepts canonical: https://glasshome.app/docs/dash/concepts section: "Getting started" updated: 2026-08-25 --- # Concepts A handful of terms come up across the docs. Skim this once and the rest reads faster. ## The pieces ### Dash (glasshome-dash) The app you actually interact with. Runs on your hardware (Pi, NUC, NAS, HAOS). Its server holds your Home Assistant login and talks to HA over WebSocket on your behalf; your browser only ever talks to that server. Contains one or more dashboards (named screens you configure). ### Hub (this site) Cloud service at glasshome.app. Hosts the widget registry, your account, and organizations. Optional for using Dash. ### Addon A packaging of Dash for Home Assistant OS or Supervised. Installed via the HA Add-on Store. Same app, easier install. ### Widget A tile on a dashboard bound to one or more HA entities. Built-in or installed from Hub. SolidJS component + Zod config. ## Where things run Dash does not need Hub to run. Hub is only required if you want to install community widgets or publish your own. ## Glossary ### Everyday terms - **Dashboard.** A named screen inside Dash. You can have multiple dashboards, each with its own layout and widget set. - **Breakpoint.** Layout slot keyed by screen width (`lg`, `md`, `sm`). Each dashboard stores one layout per breakpoint. See [Layouts](/docs/dash/layouts). - **Remote address.** A web address you reach Dash at from outside your home, through your own VPN, proxy, or tunnel. Declared in Settings. See [Remote Access](/docs/dash/remote-access). ### Publishing terms - **Scope.** Publishing namespace. Your personal scope is your username (`@username`, auto-assigned at signup and editable in your account settings), and an [organization](/docs/hub/organizations)'s scope is its slug (`@org-slug`). Widget IDs look like `@scope/widget-name`. - **Manifest.** The `manifest.json` (or inline metadata in `defineWidget`) that describes a widget: name, icon, size limits, SDK version. - **configVersion.** Integer on a widget manifest. Bumped when the config shape changes in a breaking way. Drives [migrations](/docs/widgets/widget-migrations). - **Trust badge.** `Official` (verified, installs without prompt) or `Community` (user-published, asks for consent). See [Publishing](/docs/widgets/widget-publishing). ## Data flow 1. Browser loads Dash from your local server (e.g. `http://192.168.1.x:3123`). 2. The GlassHome server opens a WebSocket connection to Home Assistant for live entity state and relays it to your browser. See [How GlassHome Connects](/docs/dash/how-glasshome-connects). 3. When you add a community widget, Dash fetches its bundle from Hub's CDN once, then caches it locally. 4. Dash config (layouts, themes, connections) is stored in a local SQLite database. No sync to Hub. > **What never leaves your network:** Entity state, your layouts, your HA URL, your devices. The GlassHome server talks to HA directly, on your behalf. Hub never sees any of it. ## Hub vs. Dash accounts You sign into Hub (this site) to publish widgets or hold your license. Dash itself doesn't require an account. It authenticates to Home Assistant via HA's own OAuth. --- title: "Widget Security" description: "How GlassHome keeps third-party widgets from leaking your home data or controlling devices you did not approve." slug: dash/widget-security canonical: https://glasshome.app/docs/dash/widget-security section: "Dashboards" updated: 2026-08-10 --- # Widget Security Community widgets are code other people wrote, running on your dashboard. GlassHome is built so that even a malicious widget cannot quietly leak your home's data or control devices you did not approve. ## The short version - **A widget cannot send your home's data to the internet.** Your browser only lets widgets talk to your Home Assistant and to GlassHome. - **A widget can only control the devices you approve when you install it.** A weather widget never gets the keys to unlock your door. - **The widget you run is exactly the one I published.** It is checked byte-for-byte before it loads. ## Why not iframes? The common way to sandbox third-party code is an iframe per widget. GlassHome deliberately doesn't: a dashboard full of iframes is slow, janky, and clunky. Instead, the rules about what a widget is *allowed to do* are enforced for it: a malicious widget in a fast, native-feeling tile still cannot phone home or touch unapproved devices. ## The layers Each layer stops a different thing. The first two are hard guarantees enforced by your browser or GlassHome; the rest raise the bar on top. ### Layer 1 — The network lock (stops data leaving) Your browser is told, up front, the only places a widget may send anything: your Home Assistant and GlassHome. It physically cannot reach any other server, so even a widget that reads your whole dashboard has nowhere to send it. **Protects against:** exfiltrating your presence, camera, lock, or sensor data to a stranger's server. ### Layer 2 — The locked-away keys (stops unapproved control) Your Home Assistant login token never reaches the app at all. GlassHome's own server holds it and talks to Home Assistant on the app's behalf, so neither a widget nor the rest of the page can read a key that is not there. To control something, a widget asks GlassHome, which first checks the request against what you approved at install. **Protects against:** any widget controlling a device you did not grant it, or reading your Home Assistant login. ### Layer 3 — Isolation and no hidden code (extra hardening) Each widget renders in its own sealed space: it cannot reach into GlassHome or other widgets, and its styling cannot bleed out. Widgets also cannot run code generated or downloaded on the fly. **Protects against:** one widget tampering with another or with the dashboard, and hidden behavior smuggled in after publish. Unlike Layers 1 and 2, this is defense-in-depth rather than an absolute guarantee. ## The permission prompt When you install a widget that uses your devices, GlassHome shows exactly what it is asking for, in plain language: > **"Climate" wants permission to: Control your thermostat** You approve that list, and only that list. If a later update wants *more*, GlassHome asks you again before it can run, never silently. If a widget tries something outside what you approved, the attempt is **blocked** and you get a notification. ## What a widget can and cannot do | A widget can… | A widget cannot… | | ------------------------------------------ | ------------------------------------------- | | Show the entities you give it | Send your data to any other website | | Control the devices you approve at install | Control devices you did not approve | | Render inside its own tile | Read your Home Assistant login | | Use your theme and styling | Reach into GlassHome or other widgets' data | | | Run hidden, downloaded-on-the-fly code | ## Not yet covered I would rather name the edges than pretend they aren't there. - **A widget can still read what is already on your dashboard.** It can see the entity values shown there; the protection is that Layer 1 leaves it nowhere to send them. - **Layer 3 is hardening, not a wall.** A determined attacker who found a browser-level escape could in theory get around it. That is exactly why the guarantees that matter most, Layers 1 and 2, do not depend on it. - **No human review of every widget yet.** Today the safety comes from the layers above plus the permissions you grant at install, not from someone reading each widget's code before publish. A review or signing step for the public catalog may come later. > **For the technically curious:** Layer 1 is a strict Content-Security-Policy egress allowlist. Layer 2: the Home Assistant access token lives only on the GlassHome server, which authenticates the relayed WebSocket itself; the app and its Web Worker hold no token. Widgets send permission-checked requests to the worker (validated with the same capability definition the consent screen showed you) rather than talking to Home Assistant directly. Layer 3: each widget renders in a closed shadow root, intrinsics are frozen, and `eval`/`Function` are disabled by CSP. Separately, every published bundle is SHA-256 verified server-side before it is served. ## Reporting a security issue Found a way around any of this? Email **[contact@glasshome.app](mailto:contact@glasshome.app)** with steps to reproduce, or reach me on the [GlassHome Discord](https://discord.gg/FJYdeDmrzv). I review every report and gladly credit researchers who report responsibly. --- title: "Home Assistant Addon" description: "Install GlassHome as a Home Assistant addon. One-click setup for HAOS and Supervised users." slug: dash/addon canonical: https://glasshome.app/docs/dash/addon section: "Installation" updated: 2026-06-09 --- # Home Assistant Addon The easiest way to run GlassHome if you're on Home Assistant OS or Home Assistant Supervised. ## Prerequisites - Home Assistant OS or Home Assistant Supervised (both include the Add-on Store with Supervisor). - A 64-bit device: Raspberry Pi 4 or 5, or an x86-64 machine with SSE4.2. See [Supported CPUs](/docs/dash/installation#supported-cpus). ## Installation 1. **Add the repository** Add GlassHome to your addon store. Add Repository > **Button didn't work? Add manually:** Open **Settings > Add-ons > Add-on Store**, click the **⋮ menu** (top-right) and choose **Repositories**. Paste this URL and click **Add**: > > ``` > https://github.com/glasshome/homeassistant-addon > ``` 2. **Install the addon** Find GlassHome in the Add-on Store and click Install. 3. **Start and access** Start the addon. Open `http://:3123` in your browser. > **No sidebar entry:** Dash does not appear as an item in the HA sidebar (ingress is not supported). Open it directly at `http://:3123`, where `` is the IP address of your Home Assistant machine. ## Updating Check **Settings > Add-ons** for available updates. You'll see a notification when a new version is available. ## Edge Channel I publish early builds here for testing before they ship in the stable release. The same repository includes an **Edge** version of GlassHome. Enable **Advanced Mode** in your HA profile to see it in the Add-on Store. Edge runs on port **3124**, so it can run side by side with the stable addon. > **Edge is for testing:** Edge builds may have rough edges. Use the stable version for your daily driver. ## Troubleshooting - **Addon not visible after adding the repository.** Refresh the Add-on Store page (the ⋮ menu has a "Check for updates" option). It can take a moment for the new addon to appear. - **Addon fails to start: port conflict.** Port 3123 may already be in use on your HA host. Check for other services listening on that port and stop them before starting GlassHome. - **Addon fails to start: unsupported architecture.** GlassHome requires a 64-bit CPU (Pi 4/5, x86-64 with SSE4.2). 32-bit installs and Pi 3 are not supported. See [Supported CPUs](/docs/dash/installation#supported-cpus). ## Related docs - [Connecting to Home Assistant](/docs/dash/connecting): pair Dash with HA after install - [Docker installation](/docs/dash/docker): alternative install for non-HAOS setups - [Quickstart](/docs/dash/quickstart): first dashboard in five minutes - [Troubleshooting](/docs/dash/troubleshooting): common install issues --- title: "Bug Reports" description: "How to report bugs in GlassHome. What to include, where to find logs, and how to help get it fixed fast." slug: dash/bug-reports canonical: https://glasshome.app/docs/dash/bug-reports section: "Troubleshooting" updated: 2026-06-09 --- # Bug Reports Something not working right? Your reports genuinely help shape GlassHome into something better. ## Where to Report Head to the [GlassHome Discord](https://discord.gg/FJYdeDmrzv) and post in the **#bug-reports** channel. ## What to Include The more context you give, the faster I can track it down. Try to include as many of these as you can: - **What happened.** Describe what you did and what went wrong. "I tapped the light widget and nothing happened" is better than "light widget broken". - **What you expected.** What should have happened instead. - **Steps to reproduce.** Can you make it happen again? Step-by-step instructions help a lot, even if they seem obvious. - **Screenshot or screen recording.** A picture is worth a thousand words. A short video of the issue is even better. - **GlassHome version.** Found in **Settings > About** at the bottom of the settings page. - **Device and browser.** What are you running GlassHome on? Phone, tablet, desktop? Which browser? This matters for layout and touch issues. - **How you installed GlassHome.** HA addon, Docker, or something else? Include the architecture (amd64, aarch64) if you know it. ## Getting Logs Logs often reveal what went wrong behind the scenes. Here's how to get them. ### HA Addon Logs Go to **Settings > Add-ons > GlassHome Dashboard > Log** in Home Assistant. Copy the output. ### Docker Logs ```bash docker logs glasshome ``` ### Browser Console Open your browser's developer tools (F12 or Ctrl+Shift+I), go to the **Console** tab, and copy any red errors. This is especially useful for UI bugs and widget issues. ## Template Feel free to copy-paste this into Discord and fill it in: ``` **What happened:** [describe the bug] **Steps to reproduce:** 1. [first step] 2. [second step] 3. [what goes wrong] **Expected behavior:** [what should have happened] **Version:** [e.g. 0.8.2] **Device:** [e.g. iPad, Chrome on Windows, Pixel 8] **Install method:** [HA addon / Docker] **Logs / screenshots:** [paste logs or attach screenshots] ``` > **Not sure if it's a bug?:** Post it anyway. If it turns out to be a question or a feature request, I'll point you in the right direction. --- title: "Changelog" description: "Release history for the GlassHome dashboard, version by version." slug: dash/changelog canonical: https://glasshome.app/docs/dash/changelog section: "Reference" updated: 2026-08-15 --- # Changelog Every dashboard release, newest first, as it ships in the Home Assistant add-on and the Docker image. Widget SDK releases live in the [SDK changelog](/docs/widgets/changelog), CLI releases in the [CLI changelog](/docs/widgets/cli-changelog), Hub API changes in the [Hub changelog](/docs/hub/changelog). Patch = bug fixes, minor = features, major = breaking config or SDK changes. Beta versions ship on the add-on's edge channel first; the stable channel gets the rolled-up release. Announcements land in [Discord](https://discord.gg/FJYdeDmrzv) `#announcements`, or subscribe to the [RSS feed](/docs/rss.xml). --- title: "Connecting to Home Assistant" description: "Pair GlassHome with your Home Assistant instance via OAuth. Setup wizard, demo mode, URL format pitfalls, and reconnection." slug: dash/connecting canonical: https://glasshome.app/docs/dash/connecting section: "Getting started" updated: 2026-08-25 --- # Connecting to Home Assistant After [installing](/docs/dash/installation), open `http://:3123`. The setup wizard runs on first launch. ## Connection methods ### OAuth (recommended) Paste your HA URL. Dash sends you to the HA login page to authorize, then redirects back automatically. No tokens to copy or rotate. ### Demo Mode Explore with simulated entities, no HA needed. Pick it from the welcome screen. Ready for the real thing? Choose Exit demo in Settings, then connect your HA from the welcome screen. ## Entering the URL The URL must include the protocol and the port HA is actually listening on. | Setup | URL example | | -------------------------------------- | --------------------------------- | | HA on the same LAN, plain HTTP | `http://homeassistant.local:8123` | | HA on a fixed IP | `http://192.168.1.100:8123` | | HA behind HTTPS (reverse proxy / Nabu) | `https://ha.example.com` | > **The URL must match what HA serves:** If HA is behind HTTPS, use `https://`. If HA is plain HTTP, use `http://`. A mismatch fails silently or returns a confusing CORS error. ## What the wizard does 1. Verifies the URL responds. 2. Redirects to HA for OAuth consent. 3. HA returns a refresh token to Dash. 4. Token is stored in Dash's local SQLite (never sent to Hub). ## Reconnecting To swap to a different HA instance, or repair a broken connection: **Settings > Connections > Reconnect**. Your dashboards and layouts are stored locally and remain after reconnecting; only the HA auth token is replaced. ## Privacy The GlassHome server talks to HA directly over WebSocket; your browser only ever talks to that server. Entity state, your HA URL, and the auth token never leave your network, and never reach Hub. See [How GlassHome Connects](/docs/dash/how-glasshome-connects). ## Troubleshooting | Symptom | Likely cause | Fix | | ------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wizard rejects the URL | Missing protocol or wrong port. | Try the URL in a browser tab first. Add `http://` or `https://` and confirm the port. | | Stuck on "Authorizing..." | Popup blocked, or HA is unreachable from your browser (not just from the Dash host). | Allow popups for the Dash URL, then confirm your browser can reach HA directly. | | Mixed-content / CORS error in console | HTTPS Dash URL + plain HTTP HA URL (or vice versa). | Options: serve HA over HTTPS (Nabu Casa or a reverse proxy), access Dash over plain HTTP on your local network, or use a VPN so both are on the same network. | | Connection drops after restart | HA was rebooted while the WebSocket was open. | Refresh the page. Dash reconnects automatically. | | Connected but entities not loading | Token user has no entity permissions in HA. | Re-authorize with a fully-permitted HA user. | Still stuck? See [Troubleshooting](/docs/dash/troubleshooting) or post in [Discord](https://discord.gg/FJYdeDmrzv) and I'll take a look. ## Related docs - [Home Assistant Addon](/docs/dash/addon) or [Docker](/docs/dash/docker): install Dash first - [Quickstart](/docs/dash/quickstart): what to do once connected - [How GlassHome Connects](/docs/dash/how-glasshome-connects): the three connections behind your dashboard, explained - [Remote Access](/docs/dash/remote-access): reach your dashboards from outside your network --- title: "Layouts" description: "Responsive grid breakpoints and managing multiple dashboards. Layouts are per breakpoint and per dashboard." slug: dash/layouts canonical: https://glasshome.app/docs/dash/layouts section: "Dashboards" updated: 2026-06-09 --- # Layouts Each dashboard keeps its own independent layout per breakpoint. Rearranging on your phone does not change the desktop layout. > **Layouts are per breakpoint:** Dash stores a separate widget arrangement for each screen-width breakpoint. Changes you make on a phone (small breakpoint) leave the tablet and desktop layouts untouched. ## Responsive Breakpoints The grid adapts columns to screen width. | Breakpoint | Width | Columns | | ---------- | ------- | ------- | | `lg` | ≥1024px | 12 | | `md` | ≥768px | 8 | | `sm` | \<768px | 4 | ## Editing All editing (moving, resizing, adding, removing widgets) happens in edit mode. See [Editing](/docs/dash/editing) for the full gesture reference and step-by-step. ## Multiple Dashboards Create dashboards in **Settings > Dashboards**. Each has its own widget set and independent layout per breakpoint. Switch between dashboards via the dock at the bottom of the screen. Manage dashboards (rename, delete, reorder) in **Settings > Dashboards**. ![Alternative dashboard layout](/assets/screenshots/home-ocean-breeze-dark-desktop.webp) --- title: "How GlassHome Connects" description: "The three connections behind your dashboard, and which one to check when something doesn't load." slug: dash/how-glasshome-connects canonical: https://glasshome.app/docs/dash/how-glasshome-connects section: "Getting started" updated: 2026-08-25 --- # How GlassHome Connects Under the hood there are three separate connections. Most problems come down to exactly one of them. Three things follow from this picture: - **Your device only talks to the GlassHome server.** If the dashboard won't load at all, that link is the problem. If it loads but nothing updates, the GlassHome-to-HA link is the problem. - **Sign-in is the one exception.** Your browser visits Home Assistant's own login page once, approves the connection, and comes back. Same pattern as "Sign in with Google", with your HA playing that role. After that, HA hands GlassHome a long-lived token; your device never holds HA credentials. - **Everything live is relayed.** State updates, commands, camera and media content all flow through the GlassHome server, which holds the one token for your home and talks to Home Assistant on your behalf. Your phone, tablet, or laptop never talks to Home Assistant directly and never holds that token. ## Several screens at once If your phone, a wall tablet, and someone else's laptop all have GlassHome open, each one gets its own live connection through the GlassHome server. They don't queue behind a single link: one screen changing a light doesn't make another screen's updates wait. Each connection only carries what that person is allowed to see. If someone's access covers a few rooms, their screen never receives updates for the rest of the house. The server also keeps one shared background connection per home, tracking which rooms and devices exist. So six open screens means six connections to Home Assistant, plus that shared one. A dozen always-on kiosks ask more of Home Assistant than a single phone, worth knowing if HA feels sluggish. ## Which address to give GlassHome - **Use the local address** (usually `http://192.168.x.x:8123`), not your remote URL (DuckDNS, Nabu Casa, your domain). A remote URL sends every tap across the internet and back, and dies with your internet connection. Some routers can't even loop back to it from inside the house. Your remote URL's job is [Remote Access](/docs/dash/remote-access), not this field. - **Prefer the IP over `.local` names** if things are flaky. `homeassistant.local` relies on mDNS discovery, which silently fails on some devices and networks; an IP always resolves. ## Signing in when this device can't reach Home Assistant 1. **At home**: use the Home Assistant sign-in. The green check means the GlassHome server can reach HA; the sign-in itself is where your device has to reach it, and if HA's login page doesn't open within a few seconds you're brought back with a note to try the IP address instead. 2. **Away, or on a kiosk that can't reach HA, with another device already signed in**: use **Quick connect**. The new device shows a short code; approve it from any signed-in device. 3. **Away and nothing signed in yet**: the very first setup must happen at home. HA's login page has to load on the device doing the first sign-in; that's an HA requirement. ## Troubleshooting: which connection is failing | What you're seeing | Problem link | What to check | | ------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------- | | Dashboard won't load at all | Device → GlassHome | Does the GlassHome address load in a plain browser tab? Try the IP instead of `.local`. | | Loads, but entities never update | GlassHome → HA | Is HA itself reachable and running? Recent restart or network change? | | Stuck on HA's login page | Device → HA (sign-in hop) | Can this device reach HA directly? Otherwise use Quick connect. | | Camera/media images fail, rest works | GlassHome → HA (media relay) | Does the entity work inside HA itself? | | Works at home, fails away | Device → GlassHome, over the internet | See [Remote Access](/docs/dash/remote-access). | ## Related docs - [Connecting to Home Assistant](/docs/dash/connecting): the sign-in step, URL formats, reconnecting - [Remote Access](/docs/dash/remote-access): reaching GlassHome from outside your home - [Concepts](/docs/dash/concepts): how GlassHome, Home Assistant, and Hub relate --- title: "Docker Deployment" description: "Deploy GlassHome with Docker Compose or standalone container. Multi-arch image, amd64 and aarch64 auto-selected." slug: dash/docker canonical: https://glasshome.app/docs/dash/docker section: "Installation" updated: 2026-09-06 --- # Docker Run GlassHome with Docker Compose or as a standalone container. The image is multi-arch: Docker picks the right build for your machine automatically (amd64 or aarch64). > **VM or older CPU?:** If Dash crashes immediately with `signal 4 / illegal instruction`, your CPU or VM profile is missing SSE4.2. See [Supported CPUs](/docs/dash/installation#supported-cpus) for the fix. ## Docker Compose ```yaml services: glasshome-dash: image: ghcr.io/glasshome/dash:latest container_name: glasshome-dash restart: unless-stopped ports: - "3123:3123" volumes: - glasshome_data:/data volumes: glasshome_data: driver: local ``` The image already listens on `3123` and stores data in `/data`, so there is nothing to configure. Set `PORT` or `DATA_DIR` only if you want a different internal port or data path, and update the right side of the `ports` mapping to match the new port. Behind a reverse proxy, also set `TRUSTED_PROXIES` to the proxy's address or network. See [Remote access](/docs/dash/remote-access). ## Direct Container ```bash docker run -d \ --name glasshome-dash \ -p 3123:3123 \ -v glasshome_data:/data \ --restart unless-stopped \ ghcr.io/glasshome/dash:latest ``` > **Don't skip the volume:** The `glasshome_data:/data` volume stores your SQLite database, dashboards, settings, and connections. All your data lives here. ## Container Image Published to `ghcr.io/glasshome/dash:latest`. Runs on amd64 (PCs, Intel/AMD servers) and aarch64 (Raspberry Pi, Apple Silicon). ## Ports Default `3123`. Override with `-p YOUR_PORT:3123`. ## Updating **Docker Compose:** ```bash docker compose pull && docker compose up -d ``` **Standalone container** (no Compose): ```bash docker pull ghcr.io/glasshome/dash:latest docker rm -f glasshome-dash docker run -d \ --name glasshome-dash \ -p 3123:3123 \ -v glasshome_data:/data \ --restart unless-stopped \ ghcr.io/glasshome/dash:latest ``` Data in the `glasshome_data` volume is not touched by either method. ## Beta builds I publish early builds for testing before they ship in a stable release (the same builds the addon's Edge channel tracks). Beta runs on its own port, container name and volume, so it sits beside a stable install with a separate database instead of upgrading the one you use every day. **Docker Compose:** ```yaml services: glasshome-dash-beta: image: ghcr.io/glasshome/dash:beta container_name: glasshome-dash-beta restart: unless-stopped ports: - "3124:3123" volumes: - glasshome_beta_data:/data volumes: glasshome_beta_data: driver: local ``` **Direct container:** ```bash docker run -d \ --name glasshome-dash-beta \ -p 3124:3123 \ -v glasshome_beta_data:/data \ --restart unless-stopped \ ghcr.io/glasshome/dash:beta ``` Open beta at `http://:3124`. It starts empty, so you run the setup wizard again and connect it to Home Assistant like a fresh install. To pin a specific build instead of floating on `beta`, pick a version from the [GHCR tags page](https://github.com/orgs/glasshome/packages/container/package/dash). > **Beta is for testing:** Beta builds may have rough edges. Keep the stable `latest` container on port 3123 as your daily driver. Beta dashboards and settings live in `glasshome_beta_data` and do not carry over to stable. New beta builds land on the same `beta` tag, so updating is the usual pull: ```bash docker compose pull && docker compose up -d ``` Standalone, `docker pull ghcr.io/glasshome/dash:beta`, then remove and re-run the container with the command above. Done testing? `docker rm -f glasshome-dash-beta`. Your stable install never noticed. ## Related docs - [Connecting to Home Assistant](/docs/dash/connecting): pair Dash with HA after install - [Home Assistant Addon](/docs/dash/addon): one-click install for HAOS users - [Quickstart](/docs/dash/quickstart): first dashboard in five minutes - [Troubleshooting](/docs/dash/troubleshooting): common install issues --- title: "Editing Your Dashboard" description: "Enter edit mode, add and remove widgets, move and resize tiles. Full gesture reference for live and edit mode." slug: dash/editing canonical: https://glasshome.app/docs/dash/editing section: "Dashboards" updated: 2026-06-09 --- # Editing Your Dashboard How to enter edit mode, arrange widgets, and what every gesture does in both live and edit mode. ### Tap the pencil The pencil icon in the header opens edit mode. A faint grid overlay appears. ### Rearrange Move, resize, add, remove. Every action saves automatically. ### Tap Done Returns to the live dashboard. No Save or Discard buttons; changes are already persisted. If you don't see the pencil, ask someone who manages this home. Editing is something they grant per person, see [People & access](/docs/dash/people-and-access). ## Gestures The same input produces different effects depending on whether you are in live mode or edit mode. | Gesture (desktop) | Gesture (touch) | Live mode | Edit mode | | -------------------------- | ------------------- | ---------------------------------------- | ------------------------------------- | | Click | Tap | Primary action (toggle, run scene) | Select widget | | Click and hold 500 ms | Long-press 500 ms | Open detail dialog (device controls) | (n/a) | | Click and hold 300 ms | Long-press 300 ms | (n/a) | Pick up tile (start drag) | | Click and drag inside tile | (detail dialog) | Adjust primary value (brightness, cover) | (n/a) | | Drag | Drag (after pickup) | (n/a) | Move tile; surrounding widgets reflow | | Drag corner grip | Drag corner grip | (n/a) | Resize tile | Pickup in edit mode is intentionally faster (300 ms vs 500 ms) because accidental drags are easy to undo by dropping in place, whereas an accidental detail dialog opening in live mode is more disruptive. > **Why mobile skips in-tile sliders:** In-tile sliders fought with page scroll on touch. No heuristic reliably told a deliberate slide from a scroll. Moving fine control into the detail dialog kept gestures predictable and page scroll snappy. On desktop the mouse uses a wheel for scrolling, so the conflict does not exist and in-tile drag stays. ## Adding In edit mode, tap **Add Widget** in the header. Pick a widget from the picker and it lands in the first free spot at the current breakpoint. Drag it where you want, then tap **Done**. For widget types, community widgets, and building your own, see [Widgets](/docs/dash/widgets). ## Moving ### Desktop Click and hold for 300 ms to pick up, then drag. Drop on any free space; surrounding widgets reflow. ### Mobile Long-press for 300 ms until you feel a haptic bump, then drag. Quick swipes scroll the page; page scrolling also works in the gaps between widgets. > **Autoscroll while dragging:** Drag a picked widget toward the top or bottom edge of the screen and the page scrolls automatically. You can place a widget anywhere on a long dashboard without dropping it. ## Resizing Drag the small grip at the bottom-right corner of any widget. The touch hit area extends beyond the visible grip so even 1x1 tiles are resizable with a finger. Each widget declares its own min and max size; the grip stops when you reach a limit. ## Removing In edit mode each widget shows an **X** in the top-right corner. Tap it and the widget is gone, with no confirmation prompt. The widget stays installed locally and can be re-added from the picker. ## Widget settings (config dialog) To configure a widget's bound entity, display options, size, or color: tap the gear or edit affordance that appears on the widget while in edit mode. The config dialog opens with fields auto-generated from the widget's schema. Changes save instantly, no apply button. --- title: "Themes" description: "7 free theme presets with light/dark mode. The theme editor (colors, radius, backgrounds, upload) requires Pro." slug: dash/themes canonical: https://glasshome.app/docs/dash/themes section: "Dashboards" updated: 2026-06-09 --- # Themes 7 built-in presets, each with light and dark variants. Apply a preset in **Settings > Theme**. Switching is instant. ## Free vs Pro | | Free | Pro | | ------------------------------------------------------------------ | ---- | --- | | All 7 presets | Yes | Yes | | Light / dark mode | Yes | Yes | | Applying custom themes already on your Dash | Yes | Yes | | Theming: editor (colors, radius), custom backgrounds, theme upload | | Yes | Presets and switching between themes are free for everyone. Custom themes themselves are free to use: if one is already on your Dash, anyone can apply it, and it keeps working if Pro lapses. Creating or editing one (theming) requires [Pro](/docs/hub/pro). ## Presets | Preset | Light / Dark | | --------------- | ------------ | | Midnight Glass | Dark | | Sunrise Studio | Light / Dark | | Forest Zen | Light / Dark | | Lavender Dreams | Light / Dark | | Coral Reef | Light / Dark | | Monochrome Pro | Light / Dark | | Ocean Breeze | Light / Dark | ![Lavender Dreams dark theme](/assets/screenshots/home-lavender-dreams-dark-desktop.webp) ## Theme editor (Pro) With [Pro](/docs/hub/pro), the editor in **Settings > Theme** unlocks below the preset grid: - **Colors.** 6 slots: primary, secondary, accent, border, card, background. Separate light and dark palettes. - **Border radius.** Slider from 0 to 2.0. Controls the roundness of cards, buttons, and widgets. - **Backgrounds.** Built-in patterns, custom images, overlay opacity, and blur amount. Start from any preset, tweak it, and save it as a custom theme on your Dash. You can also upload a theme file shared by someone else. Custom themes follow the same structure as built-in presets. --- title: "People & access" description: "Give each person and each shared screen its own access. Rooms, controls, sensitive devices, dashboards, invite links, and what happens when access ends." slug: dash/people-and-access canonical: https://glasshome.app/docs/dash/people-and-access section: "Dashboards" updated: 2026-08-23 --- # People & access One home, many people, and not everyone should be able to unlock the front door from the hallway tablet. **Settings > People & Devices** is where you decide who sees what and who can touch what. Everything below applies in GlassHome, in every widget, on every signed-in device. ## Who can do what Every person in your home has a role. Roles are about running the home; access (rooms, controls, dashboards) is set per person on top. | Role | Can | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Owner | Everything. Cannot be locked out, not even by themselves. | | Admin | Manage people and devices, household settings, theme and background, install and remove widgets, pair devices and approve codes, turn Developer Mode on. Always edits dashboards. | | Member | Use the dashboards they were given, change their own preferences (like dark mode), see their own row in People & Devices. | A few guardrails so nobody paints themselves into a corner: nobody edits their own access, admins cannot change other admins, and the owner's row is read-only for everyone else. ## Give someone their own access Open a person in **People & Devices** and adjust. Changes apply within about a second, no sign-out needed. ### Rooms Whole home, or pick rooms. Devices in rooms they were not given simply do not exist for them. ### Control Can control devices, or look only. Door locks, alarm, cameras, and garage stay off unless you switch them on, even inside their rooms. ### Dashboards All dashboards, or pick which ones. Can edit dashboards lets them rearrange and configure the ones they see. ### Access ends Never, in a day, in a week, or a date you pick. Same switch for people and for shared screens. When you add a person you pick a starting point first, then adjust: **Everything** (whole home, all controls), **Chosen rooms** (rooms you pick, sensitive devices off), or **Look, don't touch** (view only). Starting points are only a prefill; nothing is stored about them afterwards. > **People who sign in with Home Assistant:** Someone with their own Home Assistant account can be restricted like anyone else, and it holds everywhere in GlassHome. They still have their Home Assistant login though, so they can open Home Assistant's own app and see everything there. Their detail page says so. Restriction with real teeth belongs to people you invite by link and to shared screens, because those never hold a Home Assistant key. ## Invite someone without a Home Assistant login Not everyone in a home wants, or should have, a Home Assistant account. **Add person > Invite link** creates a local GlassHome account for them instead. 1. Choose **Local account**, pick a starting point, then name them and adjust what they can reach. Share the link or let them scan the QR code. 2. They open the link, pick a password, and land on the dashboards you gave them. 3. At the end they see their sign-in name, once. It is the name they type from then on, together with their password, from **Local sign in** on the welcome screen. It also shows on their row in People & Devices, so you can remind them. > **The link is the key:** Whoever holds the link can join as that person until it is used. It works once and expires after 7 days. Share it like a house key, not like a party invite. Pending invites show in People & Devices with Resend (makes a fresh link) and Cancel. There is no password reset for invited people. If one of them forgets, remove them and invite them again; their access settings take a minute to redo, their password a second. ## Shared screens and devices **Connect device** asks one question first: who is this device for? - **This device** follows your own access, and shows up under your sessions. - **Someone else** follows that person's access, and shows up under theirs. - **Shared device** (the kitchen tablet, the wall panel by the door) gets its own profile: a name, its own access, the dashboard it **shows on start**, and its own **theme** (or "Same as home"). Shared devices start with nothing ticked; pairing never copies your access onto a screen. Set it up first, then pair last by scanning the code on the screen or typing it in. Everything you set applies the moment it connects. Only admins can pair devices and approve Quick connect codes. Shared devices sit in the Devices group in People & Devices, with the same Access ends and Unpair controls. Unpair signs the screen out on the spot and it shows the connect screen again; pair it later with a new code. A change to a shared screen's start dashboard or theme shows up the next time that screen reloads. > **Upgraded from 1.1?:** Screens paired before this release became their own members, with access copied from whoever paired them. Admins see a one-time note asking to review them. Check each one and tighten what it can see and do. If one turns out to be your own phone, unpair it and connect it again as you. ## When access ends - **On a date.** Set **Access ends** on a person or a shared screen. When the date passes, their access ends just like removing them: they are signed out everywhere and see one clear screen, "Access ended", with **Sign in again** and **Sign out**. To bring them back, add them again (people with a Home Assistant login sign in again and you set their access anew; people you invited need a new invite link). - **Remove.** Removing someone signs them out of every device right away. Dashboards and widgets they set up stay; only their access goes. - **Sign out one device.** Each person's detail page lists where they are signed in. Sign out a single session without touching the rest. ## Good to know Honest limits in this release, so you are not surprised: - **Home Assistant's own app is a side door** for people who sign in with Home Assistant. GlassHome cannot close it; Home Assistant has no per-device permissions. - **Home Assistant's logbook names the linking user** for everything done through GlassHome, because GlassHome talks to Home Assistant with one household login. GlassHome's own audit line ("Emma turned on the lights") is the accurate record. - **Limited to some dashboards means edit, not create.** Someone given a few dashboards and Can edit can rearrange those, but cannot create new ones. Renaming a dashboard's address is admin only. - **Names are visible, states are not.** Restricted people can still see the names of rooms and devices that exist in the home. The state and controls of anything outside their access never reach them. - **No password reset for invited people.** Remove and re-invite. - **Removed Home Assistant people come back with full access.** Someone who signs in with Home Assistant and was removed (by you, or because their access date passed) gets back in with full access on their next Home Assistant sign-in, until you set their access again. Set it right away, or take away their Home Assistant account. - **Developer Mode and the assistant are admin only.** They read and write every dashboard, so only admins can switch them on. - **A hidden dock shortcut does not come back by itself.** Widen someone's dashboards later and they may need to re-add that dashboard to their dock. Everything else (rooms, controls, dashboards, expiry) is enforced on the GlassHome server, not in the browser. The app hides what you cannot use, and the server refuses it even if a browser asks. --- title: "Remote Access" description: "Reach your Dash from outside your home with a VPN (WireGuard, Tailscale) or your own address (reverse proxy, Cloudflare Tunnel)." slug: dash/remote-access canonical: https://glasshome.app/docs/dash/remote-access section: "Dashboards" updated: 2026-08-31 --- # Remote Access Dash runs on your hardware and is not published to the internet. To reach it from outside, you pick one of two shapes: 1. **Join your home network** with a VPN. Your device gets a home address, so Dash looks exactly like it does on the couch. 2. **Publish one address** through a reverse proxy or a tunnel you run yourself, and tell Dash to trust it. Both keep GlassHome out of the path. Nothing here is a GlassHome service. ## Pick one | Way | You open | Dash setup | Plan | | ------------------------------- | --------------------------- | --------------- | ---- | | WireGuard | your LAN address | nothing | Free | | Tailscale, subnet routes | your LAN address | nothing | Free | | Tailscale, machine name | a `.ts.net` address | add the address | Pro | | Reverse proxy at home | your domain, resolving home | add the address | Free | | Cloudflare Tunnel, your account | your domain, public | add the address | Pro | Rule of thumb: an address that lands on a home IP (`192.168.x.x`, `10.x.x.x`) is free. A public address is remote access and needs [Pro](/docs/hub/pro). ## Option 1: VPN Your phone joins the home network. The address never changes, so there is nothing to configure in Dash. ### WireGuard Classic VPN. Fast, tiny, and nobody but you is involved. Costs you one open UDP port. 1. Install the **WireGuard** add-on (Home Assistant Community Add-ons), or run WireGuard on your router. 2. Forward **UDP 51820** on your router to the machine running the add-on. UDP, not TCP. 3. In the add-on config, set `host` to a public address for your home (a DuckDNS name works), then add one peer per device and name it. 4. Start the add-on. It writes a config and a QR code per peer; scan it in the WireGuard app. 5. Turn the VPN on, then open `http://:3123`. Dash needs nothing: you arrive on a home IP, which is always trusted. ### Tailscale No port forwarding, no router work. Runs over WireGuard, coordinated by Tailscale. 1. Install the **Tailscale** add-on (or install Tailscale on the machine running Dash, if it is not the Home Assistant box). 2. Start it, open the login URL from the add-on log, and approve the machine in your tailnet. 3. Install Tailscale on your phone and sign in to the same tailnet. 4. Choose how you reach Dash: - **Subnet routes (free).** Set `advertise_routes: local_subnets` in the add-on, approve the route in the Tailscale admin console, and enable subnet routes on your phone. You then open the normal `http://192.168.1.100:3123`, and Dash needs nothing. - **Machine name (Pro).** Open `http://host.tailnet.ts.net:3123` and add that address in Dash. Tailscale addresses (`100.64.0.0/10`, `*.ts.net`) are not home addresses, so Dash counts them as remote access. ## Option 2: Your own address You run the entry point, you own the domain, and only Dash sits behind it. ### Cloudflare Tunnel on your own account No open ports: `cloudflared` dials out to Cloudflare and traffic comes back down that connection. 1. Have a domain on Cloudflare, and a Cloudflare account. 2. Install a `cloudflared` add-on (for example the community **Cloudflared** add-on) or run the `cloudflared` container next to Dash. 3. Point a hostname at Dash. In the add-on that is an `additional_hosts` entry: ```yaml additional_hosts: - hostname: dash.example.com service: http://192.168.1.100:3123 ``` 4. Start it and follow the authorization link once, so the tunnel can create the DNS record on your domain. 5. In Dash, open **Settings → Remote access → Add address** and enter `https://dash.example.com`. Cloudflare terminates HTTPS at its edge here, so Cloudflare can see this traffic. It is your account and your zone, and GlassHome is not involved. ### Reverse proxy If you already run Caddy, nginx, or Traefik, point a hostname at port `3123`. Caddy, which handles certificates and WebSockets on its own: ``` dash.example.com { reverse_proxy 192.168.1.100:3123 } ``` On nginx or Traefik, make sure the route forwards **WebSocket upgrades** (`Upgrade` and `Connection` headers). The dashboard is live over a WebSocket; without it the page loads and then stays empty. A proxy at home is only reachable from home unless you also open a port or combine it with a VPN. Its value is the tidy HTTPS address, and it is free as long as the hostname resolves to a home IP. ## What Dash needs, whatever you pick - **Expose Dash only, port `3123`.** Never publish Home Assistant. Dash talks to HA locally and forwards live state, camera images, and media on its own address. - **Declare the address.** **Settings → Remote access → Add address**, household admins only. Without it, sign-in is refused from that address. Local addresses are trusted automatically. - **The address must resolve.** A domain that does not exist yet fails the check. `localhost` and `127.0.0.1` are rejected on purpose. - **Pass `X-Forwarded-Proto: https`** if your proxy terminates HTTPS, so Dash builds its links with the scheme your browser used. - **Name your proxy.** Set `TRUSTED_PROXIES` to the address your proxy connects from (for example `192.168.1.50/32`), so Dash tells visitors apart. The Home Assistant addon needs nothing. - **Set it up at home first.** Run the setup wizard on the local address; add the remote one after. > **Signing in while away:** Use your **GlassHome account** or **Quick connect** (a code you approve from a device already signed in). Signing in with Home Assistant is a home-network flow: it redirects to your HA login page, which is not reachable from outside. ## What works while you are away Remote sessions route everything through your Dash at home, so the dashboard behaves the same as on your couch: live entity updates, controls, and media. Camera snapshots and album art work remotely too; Dash forwards them from Home Assistant itself, so no extra camera configuration is needed. ## Troubleshooting See [Troubleshooting](/docs/dash/troubleshooting#remote) for remote-access issues. ## Related docs - [How GlassHome connects](/docs/dash/how-glasshome-connects): why Dash wants your local Home Assistant address even when remote access is set up - [Connecting to Home Assistant](/docs/dash/connecting): local pairing comes first - [Concepts](/docs/dash/concepts): how Dash, HA, and Hub relate - [FAQ](/docs/dash/faq): privacy and account questions --- title: "FAQ" description: "Frequently asked questions about widget development. Open source, review process, and publishing trust." slug: widgets/faq canonical: https://glasshome.app/docs/widgets/faq section: "Support" updated: 2026-08-10 --- # FAQ Short answers for widget authors. App questions live in the [Dash FAQ](/docs/dash/faq); account and pricing questions in the [Hub FAQ](/docs/hub/faq). ## Is it open source? The widget SDK and CLI are open source. The dashboard is source-available; see the repo for license terms. ## How do widgets get reviewed? Community widgets publish immediately and are flagged with a Community badge. Users see a consent prompt before install. Official widgets go through GlassHome review. See [Publishing → Trust Badges](/docs/widgets/widget-publishing#trust-badges). ## Do I need a Hub account to build widgets? Not to build and test locally; the [CLI](/docs/widgets/widget-cli) dev flow works without one. Publishing to the registry requires an account. See [Publishing](/docs/widgets/widget-publishing). --- title: "Troubleshooting" description: "Fixes for widget development and publishing problems. SDK version mismatches, preview rendering, and publish failures." slug: widgets/troubleshooting canonical: https://glasshome.app/docs/widgets/troubleshooting section: "Support" updated: 2026-08-10 --- # Troubleshooting Author-facing fixes. User-facing dashboard problems (blank UI, entities, tunnels) live in [Dash → Troubleshooting](/docs/dash/troubleshooting). If nothing matches, ask in [Discord](https://discord.gg/FJYdeDmrzv). Users report "incompatible SDK" on install Your widget was built against a newer `@glasshome/widget-sdk` than the user's dashboard supports. - Check the `sdkVersion` range in your manifest; widen it if your widget doesn't actually need the newer APIs. - Or publish a build against the older SDK alongside the new one; the registry serves each dashboard the newest compatible version. - The dashboard-side fix (the user updating Dash) is in [Dash → Troubleshooting](/docs/dash/troubleshooting#widget-sdk). See [Widget SDK](/docs/widgets/widget-sdk) for how `sdkVersion` resolution works. Preview images missing or stale on the Hub page Previews are produced from the `examples` you declare, at publish time. - No images at all: the widget declares no `examples`. See [Widget Previews](/docs/widgets/widget-previews). - Stale images: previews regenerate per published version. Publish a new version after changing example configs; editing metadata alone does not re-render. - A broken render usually means the example config fails validation against your own schema; run `widget preview` locally to reproduce. Publish fails from the CLI - CLI below the enforced floor: Hub rejects old CLIs; the current minimum is served from `/api/widgets/cli-version`. Update with your package manager. See [Widget CLI → Version requirements](/docs/widgets/widget-cli#version-requirements). - Scope errors: publishing under an organization requires membership with the publisher role. See [Organizations](/docs/hub/organizations). - Manifest validation errors print the failing field; fix and re-run. The manifest schema is documented in [Publishing](/docs/widgets/widget-publishing). --- title: "Widget CLI" description: "All CLI commands for creating, building, testing, and publishing GlassHome widgets." slug: widgets/widget-cli canonical: https://glasshome.app/docs/widgets/widget-cli section: "CLI & Publishing" updated: 2026-08-16 --- # Widget CLI The package is `@glasshome/widget-cli`; the binary is `glasshome-widget`. Scaffolded projects pin it in `devDependencies` behind a `"widget": "glasshome-widget"` script alias, so commands below read `bun widget ` (project-local, kept current with `bun update`). Elsewhere use `bunx @glasshome/widget-cli@latest `; without `@latest`, `bunx` can reuse a stale cached version. ## Version requirements Hub enforces a minimum CLI version, currently **0.5.0**, served from `/api/widgets/cli-version`. CLIs **0.4.14** and newer fetch it on `login` and `publish` and hard-stop below the floor with an actionable message instead of a cryptic OAuth or HTTP error; older CLIs lack the check, so the floor only protects clients that can read it. On a "widget CLI is no longer supported" error, update: ```bash # one-off, no install: bunx @glasshome/widget-cli@latest publish # or update the project dev dependency: bun add -D @glasshome/widget-cli@latest ``` ## Two auth flows [`connect`](#connect) authenticates against your local Dash instance for live testing; [`login`](#login) against GlassHome Hub for publishing. Tokens are stored separately and are not interchangeable. ## Commands Scaffolds a widget project: `package.json`, `vite.config.ts`, `tsconfig`, and a starter widget whose details it prompts for (no extra `add` call needed). Run it outside an existing project. Prompts for a name and description, then creates `src/{name}/index.tsx` and `src/{name}/manifest.json`. Builds each widget into a self-contained JS bundle in `dist/` and generates `dist/registry.json` with metadata for all widgets. Checks manifests, bundle sizes, and registry consistency. Pass a name to check one widget, omit for all. Prints the project header, local registry summary, and per-widget metadata + bundle size. Live testing against a running Dash instance; widgets hot-reload as you edit. Full sequence in [Local Testing Workflow](#local-testing-workflow). Auth is an **OAuth device-code flow** against the local Dash instance; approve in the browser and the CLI stores a host-scoped bearer token. Distinct from `login`, which targets GlassHome Hub. Force a fresh device-authorization flow, discarding stored credentials. Validates, prompts for scope/widget/version (or takes flags), then builds and uploads via a presigned R2 URL. Runs `login` automatically if no token is found. Scopes come from `GET /api/widgets/scopes`: your personal scope (your username) plus any org scopes you can publish to. See [Publishing Scopes](/docs/widgets/widget-publishing#publishing-scopes). Publish only the named widget. Pre-pick the version bump; updates `version` in the widget's manifest. Pre-pick the publishing scope (e.g. `@my-team`). Authenticates with **GlassHome Hub** via OAuth PKCE (not local Dash): opens a browser, listens on `http://127.0.0.1:9274/callback` (120 s timeout), stores the token locally. `publish` runs it automatically when no token is present. Moves the project to a newer `@glasshome/widget-sdk` and syncs the `sdkVersion` in every `manifest.json` to what is then installed. In a **standalone project**: `bun add`s the SDK at `latest` (or `--to `) into whichever section already declares it, which also pulls in the `@glasshome/ui` it peers, then syncs the manifests and re-runs `bun widget validate`. In a **monorepo workspace** (a `package.json` with a `workspaces` field at or above the project): reads the SDK version from the workspace package, runs `bun install` from the root, syncs every manifest, then re-runs `bun widget validate`. Rewrites widgets' raw-Zod config (`z.object`, `widgetFields`) to the `defineConfig` + `field.*` API. Assistive, not lossy: fields it cannot translate stay raw Zod and are reported as manual TODOs; validation is never dropped. Migrate only the named widget. Preview without writing. > **Global flag:** Use `--dir ` with any project-scoped command (`add`, `build`, `validate`, `info`, `connect`, `publish`, `upgrade`, `migrate`) to point at a different widget project directory. `create` ignores it; it scaffolds into a new directory under the current working directory. ## Local Testing Workflow What `connect` does, in order: - Builds all widgets. - Runs the **OAuth device-code flow** against the Dash URL (browser opens to the Dash approval page). - Uploads each bundle to the Dash API under the `local` scope. - Registers the widgets via the Dash internal tRPC endpoint and enables dev mode. - Watches `src/`; on save, rebuilds and re-uploads the affected widget. - On `Ctrl+C`, unregisters the widgets and exits. No local HTTP server is started; Dash talks to its own API for the bundle bytes. --- title: "Getting Started" description: "Build custom GlassHome widgets with SolidJS and the widget SDK. Scaffold, develop with hot-reload, validate, and publish." slug: widgets/widget-development canonical: https://glasshome.app/docs/widgets/widget-development section: "Build Widgets" updated: 2026-08-16 --- # Getting Started A widget is a SolidJS component + a Zod config schema, wrapped with `defineWidget`. The dashboard renders the component, auto-generates the settings form from the schema, and feeds in entity state. ### Scaffold \`bunx @glasshome/widget-cli\@latest\` (no args). Creates a project and your first widget. ### Hot-reload \`bun widget connect \\` rebuilds on save and re-uploads to a running dashboard. ### Publish \`bun widget publish\` to Hub. Pick personal or organization scope. Inside a scaffolded project, `bun widget ` works (the CLI is pinned in `devDependencies`, with a `widget` script alias to `glasshome-widget`); elsewhere use `bunx @glasshome/widget-cli@latest `. Full command list: [Widget CLI](/docs/widgets/widget-cli). ## Prerequisites | Tool | Version | Notes | | ------- | ------- | ----------------------------------------------------------------------------------------------------------------- | | Bun | 1.0+ | CLI `engines` field requires `bun >=1.0.0` | | Node.js | 18+ | Required by `@glasshome/widget-sdk` (`engines.node >= 18`); Bun's bundled Node compatibility layer satisfies this | Scaffolded projects ship a `.mise.toml` pinning the versions. Install [mise](https://mise.jdx.dev) and run `mise install`. ## Quickstart 1. **Scaffold a project** ```bash bunx @glasshome/widget-cli@latest cd ``` Generates `package.json`, `vite.config.ts`, `tsconfig.json`, the `widget` script alias, and your first widget (prompts for name + description). 2. **(Optional) add more widgets** ```bash bun widget add ``` Prompts for name and description, creates `src//index.tsx` and `src//manifest.json`. 3. **Connect to a running Dash instance** ```bash bun widget connect http://homeassistant.local:3123 ``` Builds, runs an OAuth **device-code flow against your local Dash instance** (not Hub), uploads each bundle to the Dash API under the `local` scope, enables dev mode, then watches `src/`, re-uploading on save; widgets refresh without a page reload. 4. **Publish** ```bash bun widget publish ``` `publish` runs `login` automatically the first time. See [Publishing](/docs/widgets/widget-publishing). ## Project layout ``` my-widgets/ ├── package.json ├── vite.config.ts ├── tsconfig.json └── src/ ├── my-widget/ │ ├── index.tsx # Component + defineWidget │ └── manifest.json # Widget metadata └── another-widget/ ├── index.tsx └── manifest.json ``` One widget per directory under `src/`. Build outputs one self-contained JS bundle per widget plus a `registry.json` in `dist/`. ## Mental model - The dashboard handles rendering, gestures, theming, entity subscriptions, settings forms, and layout. Your widget supplies how the bound entities are visualized and what the primary tap does. - No settings UI to write: define a Zod schema; the dashboard renders the form. - No WebSocket state to manage: `useEntity` / `useEntities` read the entity ids the user picked in config and stay reactive; `useWidgetEntityGroup` aggregates them and supplies an empty state. - No Home Assistant connection or token to hold: all reads and service calls go through SDK hooks, checked against your declared [capabilities](/docs/widgets/widget-capabilities). ## Versions at a glance | Version type | Where it lives | Who bumps it | What breaks without it | | -------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | SDK version (`sdkVersion`) | `manifest.json`, as a semver range (e.g. `^1.0.0`) | `bun widget upgrade` (bumps the dep and rewrites the range) | Dash may refuse to load bundles built against a wildly different SDK | | Widget version (`version`) | `manifest.json` | `--bump` flag on `bun widget publish` | Re-publishing the same version is rejected (409) | | Config version (`configVersion`) | `manifest.json`, integer | You, when config shape has breaking changes | Without a bump, users' saved configs aren't migrated and may fail Zod parse | | CLI minimum (Hub-enforced) | Served by Hub at `/api/widgets/cli-version` | Hub (I bump it on protocol changes) | `login`/`publish` hard-stop if CLI is below the floor | ## Auth flows: connect vs. login Both flows open a browser, but they authenticate against different hosts, and the resulting tokens are stored separately, never interchangeable: ## Where to go next ### [Widget SDK guide](/docs/widgets/widget-sdk) defineWidget, Widget components, hooks, the HA data layer, entity helpers. ### [API Reference](/docs/widgets/widget-api-reference) Every public export across the SDK, /schemas, and /vite entry points. ### [Capabilities & Permissions](/docs/widgets/widget-capabilities) Declare the Home Assistant access your widget needs and how the user approves it. ### [Styling & Animation](/docs/widgets/widget-styling) Per-widget CSS bundles, Tailwind, your own CSS files, theme variables, container queries, animation. ### [CLI reference](/docs/widgets/widget-cli) Every \`bun widget \\` and what it does. ### [Publishing](/docs/widgets/widget-publishing) Auth, scopes, versioning, trust badges. ### [Config migrations](/docs/widgets/widget-migrations) Handle breaking config changes without breaking users. --- title: "Widget SDK" description: "The GlassHome widget SDK API. defineWidget, Widget components, hooks, entity bindings, and theming." slug: widgets/widget-sdk canonical: https://glasshome.app/docs/widgets/widget-sdk section: "SDK" updated: 2026-09-06 --- # 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-component): [variants](#variants) and [slot subcomponents](#slot-subcomponents) - The [``](#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>( definition: WidgetDefinition ): WidgetDefinition ``` `WidgetDefinition` shape: ```ts interface WidgetDefinition> { manifest: WidgetManifest; configSchema?: ZodType; migrate?: (config: Record, fromConfigVersion: number) => Record; 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//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 | | `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, enforced by the host. 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, type Infer, 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; function MyLightsWidget(props: { config: Config }) { const ctx = useWidgetContext(); const { openDialog, 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: 💡, title: "No lights", message: "Hold to configure", }, }); const lightData = () => aggregatedData(); const isOn = () => lightData()?.isOn ?? false; const gestures = useWidgetGestures(() => ({ tap: () => toggle(entities().map((e) => e.id)), hold: { action: openDialog }, })); onCleanup(gestures.dispose); return ( <> {props.config.title || "Lights"} {isOn() ? "On" : "Off"} { ctx.updateConfig(config); setShowDialog(false); }} /> ); } export default defineWidget({ manifest: { name: "My Lights", description: "Toggle a group of lights", icon: "mdi:lightbulb-group", minSize: { w: 1, h: 1 }, maxSize: { w: 4, h: 4 }, sdkVersion: "^1.0.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:** `` 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 `` 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 (shell background, blur, layout defaults, hover/active behavior): a built-in id string or a full `WidgetVariantConfig` object. The SDK ships three: | `variant` id | Look | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `"classic-glass"` | Default glassmorphism: blurred translucent background, gradient shell, hover/active scale. What every official `@glasshome` widget uses. | | `"minimal"` | Clean, no background effects. Tighter padding. | | `"compact-horizontal"` | Horizontal layout for short, wide tiles. | ```tsx ``` > **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. 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`. 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 `` render tree. ```ts interface ReactiveWidgetContext { isEditMode: () => boolean; updateConfig: (config: Record) => 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( options: UseWidgetEntityGroupOptions ): UseWidgetEntityGroupResult ``` **Options:** | Field | Type | Description | | -------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- | | `entities` | `Accessor` | Reactive entities array (required) | | `emptyStateConfig` | `WidgetEmptyStateConfig` | Shown when `hasEntities` is false (required) | | `aggregationMode` | `Accessor` | `"light"`, `"sensor"`, `"switch"`, `"binary-sensor"`, `"none"` | | `sensorGroupType` | `Accessor` | 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` | All entities | | `groupData` | `Accessor` | Result of `calculateGroupData`, or `null` | | `aggregatedData` | `Accessor` | Result of preset aggregation | | `emptyState` | `Accessor` | Defined when entities are missing | | `hasEntities` | `Accessor` | Whether minimum entities are present | | `count` | `Accessor` | 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 `` to wire open/close and tab state automatically. ## WidgetDialog component `` 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 (``). Do the same in your project to keep call sites short. ### Content props | Prop | Type | Purpose | | ------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `title` | `string` | Dialog title (required) | | `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` | 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` | 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 `controlsContent`. 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 300 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 ``. 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. Reads and service calls are checked against your declared [capabilities](/docs/widgets/widget-capabilities). ### Reading entities | Hook | Returns | Purpose | | ---------------------------------- | --------------------------------------------- | ------------------------------------------------------------------ | | `useEntity(id)` | `Accessor` | One entity by id (`id` may be a string or accessor) | | `useEntities(ids)` | `Accessor` | Many entities; pass an accessor returning the id array | | `useArea(id)` | `Accessor` | An area and its entities | | `useEntityHistory(id)` | `Accessor` | Recent state history for an entity | | `useEntityStatistics(id, options)` | `Resource` | Long-term statistics; the resource exposes `.loading` and `.error` | | `useForecast(id)` | `Accessor` | Weather forecast for a weather entity | | `useCamera(id)` | `{ stream, refresh }` | Reactive camera stream data | | `useStore(selector)` | `Accessor` | 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 ; } ``` ### 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` | 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` | | `useUnitSystem()` | `Accessor` | | `useTemperatureUnit()` | `Accessor` (e.g. `"°C"`) | | `useLocale()` | `Accessor` (BCP 47) | | `useCurrency()` | `Accessor` (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 ( ({ tap: () => toggle(props.config.entityId) }))}> {/* turnOn("light.x", { brightness_pct: 80 }) */} {/* callService("light", "turn_on", { brightness_pct: 80 }, { entity_id: "light.x" }) */} ); } ``` | 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`. 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 `` 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 `` 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. --- title: "Widget Config API" description: "Declare a widget's settings form with defineConfig and field.* — field kinds, type inference, and raw schemas." slug: widgets/widget-sdk/config canonical: https://glasshome.app/docs/widgets/widget-sdk/config section: "SDK" updated: 2026-08-25 --- # 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; ``` `Infer` 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.image({ title?, default? })` | Image 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" }), photo: field.image({ title: "Photo" }), }); // Config = { mode: "auto" | "heat" | "cool"; target: number; sensors: string[]; icon: string; photo?: 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. > **Images are ids, not URLs:** `field.image()` stores an opaque id for a picture the homeowner uploaded to > their dashboard, never a URL. Turn it into something an `` can load with > `imageUrl(id)` from `@glasshome/widget-sdk`: > > ```tsx > import { imageUrl } from "@glasshome/widget-sdk"; > > const src = () => imageUrl(props.config.photo); > ``` > > The resolver's general name is `mediaUrl`; `imageUrl` is the image-flavoured > alias, and the two are the same function. Either one reads fine here. > > `undefined` is a normal answer, not an error: the homeowner may not have picked > anything yet, and a preview or render host may have no media store at all. Draw > your own empty state or your own bundled art when it comes back undefined, and > handle `onError` on the `` for the picture that was deleted after it was > picked. > > There is no plural field. A carousel is `field.list` of an object wrapper > holding a `field.image` and a `field.text` caption. > **One level deep:** The settings form is a flat list of fields. `field.group` is the single > nested exception; don't nest beyond that. ## Repeating and branching fields Two field kinds build shapes the flat list can't (SDK 1.9.0+). `field.list(item, opts)` renders an add / remove / reorder list of sub-forms: ```tsx const configSchema = defineConfig({ nodes: field.list( { label: field.text({ title: "Label" }), entity: field.entity("sensor"), }, { title: "Nodes", max: 8, labelField: "label" }, ), }); // Config = { nodes: { label: string; entity: string }[] } ``` `max` is required and caps at 24. Every item is a rendered subtree and usually its own entity subscription, so an uncapped list is a performance cliff you'd hand to the user. `labelField` names the field shown on each collapsed row, and it has to exist in the item shape. A list inside a list throws when the schema is defined, including one hidden inside a `variants` branch. `field.variants(discriminator, variants, opts)` builds a tagged union, for when a config means different things depending on a kind: ```tsx const configSchema = defineConfig({ source: field.variants( "kind", { entity: { entity: field.entity("sensor") }, fixed: { value: field.number({ title: "Value", default: 0 }) }, }, { title: "Source", labels: { entity: "From an entity", fixed: "A fixed value" }, shared: { unit: field.text({ title: "Unit" }) }, }, ), }); ``` `shared` fields are merged into every branch, and `labels` name the branches in the form's kind selector. > **Adopting either is a config shape change:** Moving existing settings into a `list` or `variants` changes the stored shape, > so bump `configVersion` in `defineWidget` and write the migration. Old configs > still parse, which is exactly why the build guard, not the type checker, is > what catches a missing bump. ## 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` | `Infer` | --- title: "API Reference" description: "The complete public API of the GlassHome widget SDK. Every export across the main entry, /schemas, and /vite, with signatures." slug: widgets/widget-api-reference canonical: https://glasshome.app/docs/widgets/widget-api-reference section: "SDK" updated: 2026-09-06 --- # API Reference Every public export of `@glasshome/widget-sdk`, with signatures, by entry point. The [Widget SDK guide](/docs/widgets/widget-sdk) walks the important APIs with worked examples. Anything not listed here is internal and may change without notice. ## Entry points | Import | Use it for | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `@glasshome/widget-sdk` | Everything you use at runtime: `defineWidget`, components, hooks, the data layer, utilities, theming. | | `@glasshome/widget-sdk/schemas` | Zod schemas for validating manifests and publish payloads. Used by the CLI and Hub. | | `@glasshome/widget-sdk/vite` | Build plugins and helpers for `vite.config.ts`. The scaffold wires these for you. | | `@glasshome/widget-sdk/tailwind-sources` | A CSS file the build feeds to Tailwind. Referenced by the scaffold; not imported in widget code. | Most types named below are exported too. SolidJS conventions apply: `Accessor` is `() => T`, and an argument typed `Accessor | X` accepts a plain value or an accessor. > **Always import the data layer from the SDK:** Home Assistant hooks (`useEntity`, `useService`, …) are re-exported from the SDK on purpose. Importing them from @glasshome/sync-layer directly fails the build and would bundle a second, disconnected store. See Capabilities. --- ## `@glasshome/widget-sdk` (main entry) ### Core ```ts function defineWidget>( definition: WidgetDefinition ): WidgetDefinition ``` Declares a widget. When `definition.configSchema` is present, it auto-populates `manifest.schema` (JSON Schema) and `manifest.defaultConfig`. See [the guide](/docs/widgets/widget-sdk#definewidget). ```ts const SDK_VERSION: string // the installed package version, e.g. "1.2.0" ``` The host compares `SDK_VERSION` against each widget's `manifest.sdkVersion` range. ### Components `Widget` carries its slots as static members (`Widget.Icon`, `Widget.Title`, …); each is also exported standalone. | Export | Props (key) | What it is | | ---------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `Widget` | `variant`, `tone`, `color`, `colorTo`, `gradient`, `loading`, `emptyState`, `gestures`, `class` | The container: shell, gestures, theming. See [Widget component](/docs/widgets/widget-sdk#widget-component). | | `WidgetContent` / `Widget.Content` | `children`, `class?` | Layout wrapper. | | `WidgetIcon` / `Widget.Icon` | `icon`, `color?`, `dimmed?`, `entityCount?` | Entity icon tile with adaptive color. | | `WidgetTitle` / `Widget.Title` | `children`, `badge?` (number) | Primary label. `badge` renders a count pill when above 0. | | `WidgetStatus` / `Widget.Status` | `children`, `dimmed?`, `isUnavailable?` | State text. | | `WidgetValue` / `Widget.Value` | `value` (number \| string), `unit?`, `interpret?` | Numeric/formatted value. Takes no children: it formats `value` and appends `unit` itself, and renders `--` when `value` is missing. | | `WidgetSliderFill` / `Widget.SliderFill` | `value` (0–100), `color?`, `isDragging?` | Animated background fill for slider widgets. | | `WidgetDialog` | see [WidgetDialog component](/docs/widgets/widget-sdk#widgetdialog-component) | Settings/detail dialog. Renders no chrome of its own; inject UI via props. | ### Widget hooks ```ts function useWidgetContext(): ReactiveWidgetContext ``` Reactive widget context. Must be called inside the `` tree. ```ts interface ReactiveWidgetContext { updateConfig: (config: Record) => void; dimensions: () => { width: number; height: number }; // deprecated since 1.9.0, use useWidgetDimensions() registerDialogOpener?: (open: ((tab?: string) => void) | null) => void; callService?: ServiceCallFn; // present when capability-routed by the host } ``` `isEditMode` was removed in 1.9.0 (nothing read it; the host provided a constant `false`). ```ts function useWidgetDimensions(): () => { width: number; height: number } ``` Measured shell size in CSS px. Call it in a component **inside** ``; it throws anywhere else. `ctx.dimensions()` is deprecated (removed in 2.0.0): outside `` it silently read the host's stub, which returns `(0,0)` forever, locking a widget into its smallest layout. This hook throws instead. ```ts function useWidgetEntityGroup( options: UseWidgetEntityGroupOptions ): UseWidgetEntityGroupResult ``` Aggregate one or many entities with presets + empty state. Options: reactive `entities: Accessor`, an `aggregationMode` (`"light" \| "sensor" \| "switch" \| "binary-sensor" \| "none"`) or a custom `calculateGroupData`, plus `emptyStateConfig`, `sensorGroupType`, `allEntitiesMode`, and `minEntities`. Returns `entities`, `groupData`, `aggregatedData`, `emptyState`, `hasEntities`, and `count` accessors. Full tables in [the guide](/docs/widgets/widget-sdk#usewidgetentitygroupoptions). ```ts function useWidgetDialog(defaultTab = "controls"): WidgetDialogReturn ``` Open/close + active-tab state for `WidgetDialog`. Returns `showDialog`, `setShowDialog`, `openDialog`, `closeDialog`, `activeTab`, `setActiveTab`, and a `dialogProps` bag to spread onto ``. ```ts function useWidgetGestures( config: () => GestureConfig, orientation?: () => "horizontal" | "vertical" | "square" ): GestureHandlers ``` Tap, hold, and slide gestures for any pointer. `GestureConfig` = `{ tap?, hold?: { action, delay? }, slide?: { value, onChange, min?, max?, orientation?, activationDelay? } }`. Pass the returned `GestureHandlers` to ``; call `.dispose()` in `onCleanup`. ```ts function useReducedMotion(): Accessor // prefers-reduced-motion; false if unavailable (SSR-safe) function useIntersectionPause(el: Accessor): Accessor // true while off-screen ``` | Export | Kind | What it is | | ----------- | --------------- | ------------------------------------------------------------------------ | | `WidgetCtx` | SolidJS context | The context object behind `useWidgetContext`. Advanced; prefer the hook. | ### Config schema helpers Since 1.4.0 the config surface is `defineConfig` + `field.*` (see the [Config API](/docs/widgets/widget-sdk/config)): ```ts function defineConfig(shape: S): ConfigSchema; const field: { title(): Field; text(o): Field; number(o): Field; toggle(o): Field; choice(values: readonly T[], o): Field; entities(domain: string, o?): Field; entity(domain: string, o?): Field; area(o?): Field; stringList(o): Field; group(shape: S, o): Field>; }; type Infer; // config type from a schema, without naming zod ``` `z` is re-exported from the SDK as a permanent escape hatch for schemas `field.*` can't express. ```ts // Deprecated — use field.* / defineConfig (removed in a future major). const widgetFields: { title(): ZodType; // → field.title() entityIds(domain: string, opts?: { deviceClass?: string }): ZodType; // → field.entities(domain, opts) singleEntity(domain: string, opts?: { deviceClass?: string }): ZodType; // → field.entity(domain, opts) areaId(): ZodType; // → field.area() } ``` `widgetFields` and raw `z.object` config are deprecated. Run `bun widget migrate config`. See the [Config API](/docs/widgets/widget-sdk/config). ### Home Assistant data hooks Reactive accessors over the host's live store. Each `entityId` arg accepts a string or an accessor. Declare every domain you read in your [capabilities](/docs/widgets/widget-capabilities): that declaration is what the user sees and approves when they install your widget. | Export | Signature | What it is | | --------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `useEntity` | `(id: Accessor \| string) => Accessor` | One entity by id. | | `useEntities` | `(ids: Accessor) => Accessor` | Many entities from an id-array accessor. | | `useArea` | `(id: Accessor \| string) => Accessor` | An area and its entities. | | `useEntityHistory` | `(id) => Accessor` | Recent state history. | | `useEntityStatistics` | `(id, options) => Resource` | Long-term statistics; resource exposes `.loading` / `.error`. | | `useForecast` | `(id) => Accessor` | Weather forecast for a weather entity. | | `useCamera` | `(id) => { stream: Accessor; refresh: () => void }` | Reactive camera stream. | | `useCalendarEvents` | `(id, options?: { daysBehind?: number; daysAhead?: number }) => Accessor` | Live events for a calendar entity, pushed by Home Assistant. Window defaults to 7 days behind and 35 ahead. Data carries `events`, `loading`, `error` and the subscribed `start`/`end`. Since SDK 1.10.0. | | `useStore` | `(selector: (s) => T) => Accessor` | Direct store selector. Escape hatch. | | `byDomain` | `Accessor>` | All entity ids grouped by domain. | ### Connection & locale hooks | Export | Signature | What it is | | -------------------- | ------------------------------------------- | ----------------------- | | `useConnection` | `() => { status; isConnected }` (accessors) | Live connection status. | | `useHassConfig` | `() => Accessor` | Home Assistant config. | | `useUnitSystem` | `() => Accessor` | Unit system. | | `useTemperatureUnit` | `() => Accessor` | e.g. `"°C"`. | | `useLocale` | `() => Accessor` | BCP 47 locale. | | `useCurrency` | `() => Accessor` | ISO 4217 currency. | ### Services ```ts function useService(): { callService: ServiceCallFn; turnOn: (entityId: string | string[], data?: Record) => Promise; turnOff: (entityId: string | string[], data?: Record) => Promise; toggle: (entityId: string | string[], data?: Record) => Promise; } type ServiceCallFn = ( domain: string, service: string, data?: Record, target?: { entity_id?: string | string[] }, options?: { returnResponse?: boolean } ) => Promise; ``` Host-mounted widgets route these through the host's capability check (a `control` grant is required); preview/tests fall back to direct sync-layer calls. `useTurnOn()`, `useTurnOff()`, and `useToggle()` each return just the one shortcut. Pass `{ returnResponse: true }` for services that return data, such as `todo.get_items`; the promise then resolves to Home Assistant's service response instead of `undefined`. Only set it for services that support a response, since HA errors otherwise. The capability check is unchanged. Since SDK 1.10.0, and the host forwards the response from **Dash 1.1.0-beta.5** onward — on older hosts the call still runs but resolves `undefined`. ### Entity utilities | Export | Signature | What it is | | ---------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `isEntityActive` | `(entity: EntityView \| null \| undefined) => boolean` | `true` for active states. | | `getEntityAttribute` | `(entity, name: string, fallback?: T) => T \| undefined` | Read one attribute with a fallback. | | `hassMediaUrl` | `(path: string \| null \| undefined) => string \| undefined` | Resolve an HA media path (album art, camera image) to a loadable URL. See [Media URLs](#media-urls). | | `countActiveEntities` | `(entities: EntityView[]) => number` | Count active entities. | | `calculateLightGroup` | `(entities: EntityView[], allEntitiesMode?= false) => LightGroupResult` | Aggregate on/off, brightness, color across a group. | | `calculateSensorGroup` | `(entities: EntityView[], groupType?: SensorGroupType = "mean", ignoreNonNumeric?= true) => SensorGroupResult` | Aggregate numeric sensors. | `LightGroupResult`: `isGroup`, `state`, `isOn`, `isUnavailable`, `brightness` (0–255), `brightnessPercent` (0–100), `color`, `onCount`, `totalCount`, `description`. `SensorGroupResult`: `isGroup`, `state`, `numericValue` (`number \| null`), `isUnavailable`, `unit?`, `description`, `memberValues?`. `SensorGroupType`: `"min" \| "max" \| "mean" \| "median" \| "sum" \| "last" \| "range" \| "product" \| "std_dev"`. ### Media URLs Home Assistant exposes media references (album art `entity_picture`, camera proxy paths) as origin-relative paths like `/api/media_player_proxy/...?token=...`, which a bare `` resolves against the dashboard's own origin and fails to load. `hassMediaUrl` resolves them through the host's same-origin media proxy, so the image loads both **at home and over remote access** (no extra permissions, the path keeps Home Assistant's own signed token). ```tsx import { hassMediaUrl, useEntity, getEntityAttribute } from "@glasshome/widget-sdk"; const entity = useEntity(entityId); const albumArt = () => { const e = entity(); if (!e) return undefined; return hassMediaUrl(getEntityAttribute(e, "entity_picture")); }; ; ``` - Absolute `http(s)://` URLs pass through unchanged. - Missing or empty input returns `undefined`, so `` handles "nothing playing" for free. - No external service (cover-art API etc.) is needed for media Home Assistant already serves: read `entity_picture` and wrap it. - Available since widget-sdk **1.3.0**. `imageUrl` and `mediaUrl` do a different job: they resolve a household media-library id, which is what [`field.image()`](/docs/widgets/widget-sdk/config) stores, through the host's media store. Both are re-exported from `@glasshome/ui` since **1.12.0**. ### Theming & charts | Export | Signature | What it is | | ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `isDark` | `() => boolean` | Current theme as a boolean. | | `injectTokens` | `(root?: Document \| ShadowRoot) => void` | Attach SDK CSS tokens to a root the host did not set up. | | `svgColors` | `Record` | Energy chart colors. Keys: `solar`, `grid`, `battery`, `ev`, `home`, `positive`, `negative`. See [Charts](/docs/widgets/widget-styling#charts). | | `ToneSchema` | Zod enum | Validates a `Tone`. | | `monotoneCubicPath` | `(points) => string` | Smooth SVG path string from a list of points. | `Tone`: `"success" \| "warning" \| "danger" \| "info" \| "neutral" \| "accent"`. ### Validation re-exports Convenience re-exports also available from [`/schemas`](#glasshomewidget-sdkschemas). | Export | Kind | What it is | | ---------------------- | ----------------- | ----------------------------------------- | | `WidgetManifestSchema` | Zod schema | Validate a complete widget manifest. | | `formatSchemaError` | `(err) => string` | Turn a Zod error into a readable message. | ### Deprecation registry The single source of truth for deprecated APIs, driving warn-once runtime notices, the CLI lint, and the deprecation docs table. | Export | Kind | What it is | | ------------------- | ------------------------------------- | ------------------------------------------------------- | | `deprecations` | `DeprecationEntry[]` | The registry of deprecated APIs and their replacements. | | `formatDeprecation` | `(entry: DeprecationEntry) => string` | Render one entry as a readable warning message. | | `DeprecationEntry` | type | One deprecation record. | ### Lower-level data exports Building blocks under the hooks; most widgets never touch these. | Export | Kind | What it is | | --------------------------------------------- | ----------------------- | -------------------------------------------- | | `state` | `Store` | The raw reactive store. | | `getForecasts` | function | Imperative forecast fetch. | | `trackEntityHistory` / `untrackEntityHistory` | `(entityId) => void` | Start / stop history tracking for an entity. | | `getStream` | function | Camera stream descriptor. | | `getWebRtcClientConfig` | function | WebRTC client config for camera streaming. | | `startWebRtcSession` | function | Begin a WebRTC camera session. | | `sendWebRtcCandidate` | function | Send an ICE candidate during a session. | ### Types (main entry) ```ts interface WidgetDefinition> { manifest: WidgetManifest; configSchema?: ZodType; migrate?: (config: Record, fromConfigVersion: number) => Record; component: (props: { config: C }) => any; } interface WidgetManifest { name: string; description?: string; minSize: GridSize; // { w, h } maxSize: GridSize; defaultSize?: GridSize; sdkVersion: string; icon?: string; capabilities?: CapabilityGrant[]; configVersion?: number; schema?: object; // auto-populated from configSchema; do not set defaultConfig?: Record; // auto-populated; do not set cssUrl?: string; // set by the build } interface GridSize { w: number; h: number } ``` Other exported types: `WidgetContext`, `ReactiveWidgetContext`, `EntityView` (see [the shape](/docs/widgets/widget-sdk#the-entityview-shape)), `AreaView`, `WidgetVariantConfig`, `WidgetStyles`, `Tone`, `SvgColorKey`, `GestureHandlers`, `AggregationPreset`, `SensorGroupType`, `LightGroupResult`, `SensorGroupResult`, `UseWidgetEntityGroupOptions`, `UseWidgetEntityGroupResult`, `WidgetDialogProps`, `WidgetDialogTab`, `WidgetDialogReturn`, `ServiceCallFn`. --- ## `@glasshome/widget-sdk/schemas` Zod schemas re-exported from the canonical `@glasshome/widget-contract` package, the single source of truth shared by the SDK, CLI, Hub, and Dash, so the same definitions validate a manifest everywhere. | Export | Kind | What it is | | --------------------------------------------------------------------- | ----------------- | -------------------------------------------------- | | `widgetManifestSchema` (alias `WidgetManifestSchema`) | Zod schema | Validate a complete widget manifest. | | `publishManifestSchema` | Zod schema | Validate the manifest subset sent at publish time. | | `capabilitiesSchema` | Zod schema | Validate a `capabilities` array. | | `capabilityGrantSchema` | Zod schema | Validate one `{ domain, access }` grant. | | `GridSizeSchema` | Zod schema | Validate `{ w, h }`. | | `parseGridSize` / `serializeGridSize` | functions | Convert grid size to/from the stored JSON form. | | `PublishRequestSchema` / `PublishConfirmSchema` / `PublishBodySchema` | Zod schemas | Publish-flow request bodies. | | `formatSchemaError` | `(err) => string` | Turn a Zod error into a readable message. | | `CapabilityGrant` | type | One capability grant. | --- ## `@glasshome/widget-sdk/vite` Build plugins and helpers for `vite.config.ts`. The scaffolded config wires these; reach for them only in custom setups. See [Build tooling](/docs/widgets/widget-sdk#build-tooling). | Export | Signature | What it is | | ------------------ | -------------------------------------------------- | --------------------------------------------------------------- | | `glasshomeWidget` | `(options?: GlasshomeWidgetOptions) => Plugin[]` | Single-widget plugin: dev preview + library build. | | `glasshomeWidgets` | `(options?: GlasshomeWidgetsOptions) => Plugin[]` | Multi-widget plugin: per-widget builds + `registry.json`. | | `buildWidgets` | `(options?: BuildWidgetsOptions) => Promise` | Programmatic build of all widgets (used by `bun widget build`). | | `discoverWidgets` | `(srcDir: string) => DiscoveredWidget[]` | Scan `src/` for subdirs with `index.tsx` + `manifest.json`. | | `generateRegistry` | `(srcDir: string, outDir: string) => void` | Write a `registry.json` from discovered manifests. | | `isWidgetExternal` | `(id: string) => boolean` | `true` for host-provided packages (kept out of the bundle). | Option shapes (all exported): - `GlasshomeWidgetsOptions`: `{ srcDir?, outDir? }` only. - `BuildWidgetsOptions`: `{ srcDir?, outDir?, plugins?, viteConfig?, only? }` (`plugins`, `viteConfig`, and `only` are `buildWidgets`-only; `glasshomeWidgets` does not accept them). - `GlasshomeWidgetOptions`: `{ entry? }` (defaults to `"src/index.tsx"`). --- ## UI primitives (re-exported from `@glasshome/ui`) Import these from `@glasshome/widget-sdk`, not from `@glasshome/ui/solid`. The SDK re-export is the one `sdkVersion` gates; direct imports are deprecated since **1.9.0** and removed in **2.0.0**. `WidgetDialog` takes its chrome as props and renders none of its own. Pass `ResponsiveDialog`, `ResponsiveDialogContent`, `ResponsiveDialogHeader`, `ResponsiveDialogTitle`, `ResponsiveDialogDescription` and `Button`; add `SchemaForm` when you use `configSchema`. See [WidgetDialog component](/docs/widgets/widget-sdk#widgetdialog-component). Keep `@glasshome/ui` installed either way, since the build reads its styles. The full re-export set, with the version each one arrived in, is in [Styling](/docs/widgets/widget-styling#importing-ui-components). --- title: "Capabilities & Permissions" description: "How a widget declares the Home Assistant access it needs. The capability grammar, access levels, narrowing, user consent, and host enforcement." slug: widgets/widget-capabilities canonical: https://glasshome.app/docs/widgets/widget-capabilities section: "Build Widgets" updated: 2026-09-03 --- # Capabilities & Permissions A widget runs sandboxed: an isolated shadow root, no Home Assistant connection or token, no network beyond Home Assistant and the Hub. To read or control anything in the home, it **declares the access it needs** in its manifest. The user approves that list at install, and the host enforces it on every call. Access is **deny-by-default**: anything you don't declare is blocked at runtime. > **Info:** This is the developer's view. For the homeowner-facing explanation of the sandbox, see Widget Security. ## How access works Home Assistant interaction goes only through the SDK's [data and service hooks](/docs/widgets/widget-sdk#home-assistant-data--services): importing `@glasshome/sync-layer` directly fails the build, and `fetch`/WebSocket to Home Assistant is blocked. The host checks every call against your declared capabilities before touching Home Assistant. **Reads** (entity state, history, forecasts) require a `read` grant for the domain; **service calls** require `control`. ## Declaring capabilities 1. **List what the widget needs, minimally** Decide which Home Assistant domains the widget touches and whether it only *shows* them (`read`) or also *operates* them (`control`). Ask for the least that works. 2. **Add a capabilities array to the manifest** Each grant is `{ domain, access }`, plus optional narrowing: ```jsonc // manifest.json "capabilities": [ { "domain": "light", "access": "control" }, { "domain": "sensor", "access": "read" } ] ``` The same array goes in the inline `manifest` passed to `defineWidget`. A widget that touches no Home Assistant data (a clock, say) declares an empty array: ```jsonc "capabilities": [] ``` Targeting SDK 1.x **without** a `capabilities` array is rejected at publish. A manifest may declare at most 32 grants. 3. **Use the data layer** Read and control through the SDK hooks: ```tsx import { useEntity, useService } from "@glasshome/widget-sdk"; const light = useEntity(() => props.config.entityId); // needs read/control on "light" const { toggle } = useService(); // toggle(props.config.entityId) // needs control on "light" ``` ## Access levels | `access` | Grants | Use it for | | --------- | ------------------------------------------------------------ | ---------------------------- | | `read` | Reading state, attributes, history, forecasts for the domain | Widgets that only display | | `control` | Read **and** calling services on the domain | Widgets that operate devices | `control` implies `read` on the same domain, so you never need both. ## Narrowing a grant A grant covers a whole domain by default; tighten it when the widget needs only part of one. ### To specific entities `entities` lists entity-id patterns. The domain part stays literal and `*` globs only the object id, so a pattern can never widen a grant past its domain. ```jsonc { "domain": "light", "access": "control", "entities": ["light.living_*", "light.kitchen"] } ``` An entity-scoped grant cannot reach **domain-wide** services (a service call with no entity target). Leave `entities` off if the widget needs the whole domain. ### To specific services `services` restricts a `control` grant to named services; calls outside the list are rejected. ```jsonc { "domain": "media_player", "access": "control", "services": ["media_play", "media_pause"] } ``` `entities` and `services` must be non-empty when present. A service-narrowed grant covers its entities for its own domain's services only, so keep it off any grant an integration service has to reach through (see below). ## Integration services Some integrations add services that act on **another domain's entities**. Music Assistant and Sonos own no entities of their own: their speakers appear as `media_player.*`, and services like `music_assistant.get_queue` or `sonos.snapshot` are aimed at those entities. A call like that names two domains, and each needs a grant: ```jsonc "capabilities": [ { "domain": "media_player", "access": "control" }, // the speaker it acts on { "domain": "music_assistant", "access": "control" } // the service it calls ] ``` The host checks both: the service's own domain must be granted, and every entity the call targets must be covered by some grant. Granting `media_player` alone does not let a widget call Music Assistant services; granting `music_assistant` alone reaches no speaker. Narrowing still applies. With `{ "domain": "media_player", "access": "control", "entities": ["media_player.kitchen"] }`, integration services reach the kitchen speaker and nothing else. > **Info:** Services that take no entity at all (`music_assistant.get_library` takes a `config_entry_id`) act on the integration itself. Those need an un-narrowed grant on the integration's domain, the same rule as any other domain-wide service. ## What the user approves The install consent screen renders each grant as a plain-language sentence, generated from the same object the host enforces, so the prompt cannot drift from what is allowed: | Grant | Shown to the user | | --------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | `{ domain: "light", access: "control" }` | Control your lights | | `{ domain: "sensor", access: "read" }` | Read your sensors | | `{ domain: "light", access: "control", entities: ["light.living_*"] }` | Control your lights (light.living\_\*) | | `{ domain: "media_player", access: "control", services: ["media_play","media_pause"] }` | Control your media players — only: media play, media pause | ## Enforcement and updates - **Deny-by-default.** Every read and service call is checked against your grants at runtime; anything unmatched is blocked and the user is notified. Widget code cannot opt out. - **Widening re-prompts.** An update asking for more than the user approved (a new domain, broader access, removed narrowing) must be re-approved before it runs. Narrowing or unchanged grants install silently. - **Verified on publish.** The Hub validates your capability grammar and the bundle hash before a version goes live. ## See also ### [Data & service hooks](/docs/widgets/widget-sdk#home-assistant-data--services) The SDK hooks every capability gates: useEntity, useService, and the rest. ### [Widget Security](/docs/dash/widget-security) The homeowner-facing view of the sandbox and what it guarantees. --- title: "Publishing Widgets" description: "Publish your GlassHome widgets to Hub. Login, validate, version, select scope, and publish." slug: widgets/widget-publishing canonical: https://glasshome.app/docs/widgets/widget-publishing section: "CLI & Publishing" updated: 2026-06-09 --- # Publishing Widgets Share your widgets with the GlassHome community by publishing them to Hub. ## Publish Flow 1. **(Optional) Log in to GlassHome Hub** You only need to run this explicitly if you want to authenticate before publishing. The `publish` command auto-runs login if no token is found, so this step is optional in practice. ```bash bun widget login ``` This runs an OAuth PKCE flow against Hub. A browser opens; the callback lands on `http://127.0.0.1:9274/callback`. The resulting token is stored locally and reused on subsequent publishes. This is separate from `bun widget connect`, which authenticates against your local Dash instance using a device-code flow. 2. **Validate your widgets** Checks manifests, bundle sizes, and SDK version compatibility. Fix any issues before publishing. ```bash bun widget validate ``` 3. **Publish** The CLI validates first, then prompts you to select which widget to publish, choose a scope, and pick a version bump. It then builds and uploads the bundle to Hub via a presigned R2 URL. ```bash bun widget publish ``` Use flags to skip prompts: - `--name `: publish only the named widget. - `--bump keep|patch|minor|major`: pre-pick the version bump. This updates the `version` field in `src//manifest.json`. - `--scope `: pre-pick the publishing scope. ## Publish lifecycle The gate checks in the diagram above are enforced server-side, with precise failures: - **Bundle > 2 MB** → rejected with `400`. - **Duplicate version** → `409`; published versions are immutable. - **Scope you don't own** → rejected. Org scopes require the `owner` or `admin` role. ## Versioning The `version` field lives in `src//manifest.json`. The `--bump` flag increments it automatically. Versions follow semver: - **Patch (0.1.0 → 0.1.1).** Bug fixes, no config changes. - **Minor (0.1.0 → 0.2.0).** New features, backward-compatible config. - **Major (0.2.0 → 1.0.0).** Breaking config changes (bump `configVersion` too and add a `migrate` function; see [Config Migrations](/docs/widgets/widget-migrations)). > **On SDK 1.0:** Widgets must target `@glasshome/widget-sdk` 1.x and declare a `capabilities` block, or publish is rejected. See [Upgrading to SDK 1.0](/docs/widgets/widget-sdk-1-0). ## Publishing Scopes When you publish, the CLI prompts you to select a publishing scope if you belong to any organizations: ``` ? Select publishing scope: ❯ @your-username (personal) @my-team (organization) ``` Your **personal scope is your username** (`@username`). Organization scope uses the org slug. You can create organizations on [GlassHome Hub](/docs/hub/organizations). The CLI learns which scopes you can publish to from `GET /api/widgets/scopes`, which returns your personal scope plus any org scopes you have permission to publish under. ### How your username is determined Your username is assigned automatically at signup by slugifying your display name (lowercased, with non-alphanumeric runs collapsed to dashes, so "Alice Test" becomes `alice-test`). It is unique per account: if someone already has that slug, later accounts get a numbered suffix (`alice-test-2`). The username is decoupled from your display name. Renaming your display name later does **not** change your scope, so widgets you have already published stay reachable at the same `@username/widget-name` ID. You can change your username in your [account dashboard](/dashboard) settings, but **not while you have widgets published** under your current scope (changing it would orphan them). Yank or remove those widgets first, or publish future versions under a new scope. `@glasshome` is the reserved official scope and cannot be claimed by users or organizations. ## Yank, deprecation, and release notes The database schema tracks `yanked`, `yankedReason`, `deprecationMessage`, and `releaseNotes` per version. There is no user-facing mechanism to yank or deprecate a version yourself yet. If you publish a broken version and need it pulled, contact me directly and I'll yank it. `releaseNotes` will be shown in the widget detail view once that UI is built. ## After Publishing Your widget appears in the Widget Browser, installable in one click. To ship an update, bump the version and run `publish` again. If your widget declares `examples`, its preview images are produced for this version and appear on its Hub page and in the Widget Browser. See [Widget Previews](/docs/widgets/widget-previews). ## Trust Badges - **Official.** Maintained by GlassHome. Installed without a confirmation dialog. - **Community.** Published by users. Users see a consent dialog before installing. > **Bundle size:** Keep your widget bundles small. The hard limit is 2 MB (rejected at publish time). The CLI shows bundle sizes during `validate` and `info`. Large bundles slow down dashboard loading for everyone. --- title: "CLI Changelog" description: "Notable @glasshome/widget-cli releases and the Hub-enforced minimum version." slug: widgets/cli-changelog canonical: https://glasshome.app/docs/widgets/cli-changelog section: "CLI & Publishing" updated: 2026-08-16 --- # CLI Changelog Highlights of `@glasshome/widget-cli`; the entry-by-entry history lives in the [CLI CHANGELOG](https://github.com/glasshome/widget-cli/blob/main/CHANGELOG.md). The SDK ships separately: see the [SDK changelog](/docs/widgets/changelog). ## Notable CLI releases ### 0.11: upgrade upgrades - `upgrade` in a standalone project now moves the SDK for you: `bun add @glasshome/widget-sdk@latest` (or `--to `) into the section that already declares it, then the manifest `sdkVersion` sync and `validate` it always did. Before, it only synced manifests and printed the four manual steps. - Fresh projects build again on Windows and get `@glasshome/ui` on install; both fixes ship in widget-sdk 1.10.3, which `create` now scaffolds against. ### 0.10: types checked, compatibility told the truth - `build` and `publish` typecheck the project first and stop on a type error. Scaffolded projects ran a bare `vite build`, which strips types without checking them, so a widget could publish with its config type and its `configSchema` disagreeing. Projects without TypeScript are not blocked, only warned. - New widgets no longer scaffold a stale `sdkVersion`. `create` and `add` wrote a hardcoded `^0.2.0` while the project built against a 1.x SDK, and because a pre-1.0 range reads as "pre-capabilities", those widgets were quietly excused from declaring [capabilities](/docs/widgets/widget-capabilities) at all. The range is now derived from the SDK you actually have installed. - `validate` compares the manifest range against the installed SDK instead of the range in `package.json`, which only ever compared a claim with itself. `publish` runs the same check, where it was previously skipped. - `publish` sends the manifest written by the build it just ran, so bundle-owned keys no longer ship one publish behind. ### 0.9: preview shots - `glasshome-widget preview` renders every `examples` entry from your manifest, light and dark, into `preview/`, using the same harness the Hub's render worker uses. See [Widget Previews](/docs/widgets/widget-previews). - Playwright is an optional peer: without it the command prints the install line instead of failing. ### 0.8: update and compatibility warnings - Every command nudges when a newer CLI is published, checked at most once a day and cached, so warm runs stay offline. Opt out with `GLASSHOME_NO_UPDATE_NOTIFIER` or `CI`. - `validate` warns when a manifest's `sdkVersion` range excludes the SDK pinned in `package.json`. - `login` stores the Hub-assigned username as your publish scope, so the scope shown at login matches what `publish` uses. ### 0.6: config migration - `migrate config` rewrites a widget's raw-zod config to the SDK config API (`defineConfig` + `field.*`). Unrecognized patterns are left alone and reported. `--dry` previews; `--name ` targets one widget. See [Config API](/docs/widgets/widget-sdk/config). - `build` and `connect` lint for deprecated config usage and direct `zod` imports, printing the removal timeline. Warning only. ## Minimum version Hub enforces a minimum CLI version per deploy; publishing with an older CLI is refused. The current floor is served from `/api/widgets/cli-version`. See [Widget CLI](/docs/widgets/widget-cli#version-requirements). ## Releases [`@glasshome/widget-cli` on npm](https://www.npmjs.com/package/@glasshome/widget-cli) --- title: "Upgrading to SDK 1.0" description: "What changed in widget-sdk 1.0 (shadow-root rendering, separate CSS, capability declarations, and no direct Home Assistant access) and how to migrate a widget." slug: widgets/widget-sdk-1-0 canonical: https://glasshome.app/docs/widgets/widget-sdk-1-0 section: "SDK" updated: 2026-06-12 --- # Upgrading to SDK 1.0 `@glasshome/widget-sdk` 1.0 is the security release. Widgets now run inside an isolated, permission-scoped sandbox. Most widgets that only use SDK hooks need small, mechanical changes. This page is the migration checklist. > **Info:** Dash only loads widgets built against SDK 1.x. A widget published against an older SDK shows a **"needs update"** tile until you republish. Bump your `@glasshome/widget-sdk` dependency to `1.x`, work through this page, then `bun widget publish`. ## What changed ### Shadow-root rendering Each widget renders in its own closed shadow root for style and DOM isolation. Your CSS no longer leaks into Dash or other widgets, and vice versa. ### Separate stylesheet The build emits index.css next to index.js. Dash adopts it into your widget's shadow root. CSS is no longer inlined into the JS bundle. ### Capability declarations Your manifest declares which Home Assistant domains the widget reads or controls. The user approves them at install; the host enforces them. ### No direct HA access Widgets no longer import @glasshome/sync-layer or call fetch/WebSocket directly. All Home Assistant interaction goes through SDK hooks, which are permission-checked. ## Migration checklist ### 1. Bump the SDK dependency ```jsonc // package.json "peerDependencies": { "@glasshome/widget-sdk": "1.0.0" } ``` And set the manifest range so Dash knows the widget targets 1.x: ```jsonc // manifest.json "sdkVersion": "^1.0.0" ``` ### 2. Import data and service hooks from the SDK, not sync-layer Direct `@glasshome/sync-layer` imports now **fail the build** with a clear error. The SDK re-exports every hook you used: ```diff - import { useEntity, useService } from "@glasshome/sync-layer/solid"; + import { useEntity, useService } from "@glasshome/widget-sdk"; ``` This is what makes permissions enforceable: service calls route through the host's permission check instead of straight to Home Assistant. ### 3. Declare capabilities Add a `capabilities` array to your manifest listing the Home Assistant domains your widget touches, `control` for ones you operate and `read` for ones you only display. A widget that never touches Home Assistant declares `[]`. Publishing a 1.x widget without the array is rejected. ```jsonc // manifest.json "capabilities": [ { "domain": "light", "access": "control" }, { "domain": "sensor", "access": "read" } ] ``` The full grammar (access levels, narrowing to specific entities or services, what the user approves, and how the host enforces it) is covered in **[Capabilities & Permissions](/docs/widgets/widget-capabilities)**. Declare the minimum: a widening update has to be re-approved by the user. ### 4. Don't fight the shadow root Your widget now renders in an isolated shadow root with its own CSS bundle. Theme variables still inherit, so don't redefine them; keep `@glasshome/ui` as a dependency; and reach Home Assistant through SDK hooks rather than `window`/`document` globals. The full styling model is in **[Styling & Animation](/docs/widgets/widget-styling)**. ### 5. Rebuild and publish ```bash bun run build # emits index.js + index.css + an updated registry bun widget publish --scope ``` The publish flow verifies the bundle hash server-side and validates your capability declaration before the version goes live. ## Testing locally first `bun widget connect ` builds and serves your widget to a running Dash, including the capability flow: install it, and you'll see the same permission prompt your users will. Confirm the widget renders correctly inside its shadow root (styling, theme, dialogs) and that its service calls work before publishing. --- title: "Styling & Animation" description: "How widget styling works in the GlassHome SDK, covering per-widget CSS bundles, Tailwind utilities, your own CSS files, theme variables, container queries, and animation." slug: widgets/widget-styling canonical: https://glasshome.app/docs/widgets/widget-styling section: "Build Widgets" updated: 2026-09-06 --- # Styling & Animation Each widget renders in its own **closed shadow root** and carries its own complete CSS: the build writes one `.css` next to `.js`, which the host adopts into the shadow root at mount. Isolation runs both ways: your styles never leak into the dashboard or other widgets, the dashboard's never reach you, and everything you render must be styled from your bundle. ### Tailwind, built in The full Tailwind v4 utility set plus tw-animate-css. Only the classes you actually use ship in your bundle. ### Your own CSS Import a .css file from your widget and it gets bundled into the widget stylesheet, scoped to your shadow root. ### Theme follows the home The user's theme variables inherit through the shadow boundary. Use them; never redefine them. ## What's in the bundle The SDK runs Tailwind over exactly three sources into your widget's stylesheet: 1. **Your widget's own files** (the directory of your `index.tsx`), so your JSX utility classes are included. 2. **`@glasshome/ui`**: the theme and component styles. Keep it installed so the build can read them, but import its components from `@glasshome/widget-sdk` ([why](#importing-ui-components)). It is a peer of the SDK with its own version line, so `bun install` pulls in the version your SDK was built against. The build stops with `@glasshome/ui is required` when it is missing. 3. **The SDK shell styles**: the `.glasshome-widget-*` classes and CSS variables the `Widget` components use. Tailwind runs with `source(none)`, so there is no automatic project-root scan: each widget gets **only the utilities it uses**, not the superset of every widget in your repo. Classes must appear literally in your source, so build class names statically, not by runtime concatenation. ## Importing UI components Import GlassHome's UI primitives **from the SDK**, not from `@glasshome/ui`: ```tsx // Do this import { Badge, Button, Slider, SchemaForm } from "@glasshome/widget-sdk"; // Not this import { Badge, Button, Slider, SchemaForm } from "@glasshome/ui/solid"; ``` Both resolve to the same components at runtime. The difference is version checking. `sdkVersion` in `manifest.json` is the only compatibility gate. The dashboard refuses to mount a widget whose declared range excludes the SDK it ships. `@glasshome/ui` has no such gate. The dashboard serves it directly, and widgets update from the Hub independently of the dashboard they land on, so a direct import can meet a version it was never built against, unchecked. Routed through the SDK, the same components sit behind `sdkVersion`. The failure is not graceful: a missing export is a module **link** error, so the widget never evaluates. No error boundary, no partial render, a dead tile. The re-exported set only grows. The version column is when each export arrived: | Group | Exports | Since | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | Controls | `Badge`, `Button`, `Input`, `Label`, `Slider`, `Switch` | 1.9.0 | | Select | `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` | 1.9.0 | | Color | `Color` (type), `ColorSlider`, `ColorWheel`, `parseColor` | 1.9.0 | | Forms | `SchemaForm` | 1.9.0 | | Dialog | `ResponsiveDialog`, `ResponsiveDialogContent`, `ResponsiveDialogDescription`, `ResponsiveDialogHeader`, `ResponsiveDialogTitle` | 1.9.0 | | Dialog parts | `ResponsiveDialogBody`, `ResponsiveDialogClose`, `ResponsiveDialogFooter` | 1.14.0 | | Carousel | `Carousel`, `CarouselApi` (type), `CarouselContent`, `CarouselDots`, `CarouselItem`, `CarouselNext`, `CarouselPrevious`, `CarouselTransition` (type) | 1.12.0 | | Tabs | `Tabs`, `TabsContent`, `TabsList`, `TabsTrigger` | 1.14.0 | | Media | `imageUrl`, `mediaUrl` | 1.12.0 | `imageUrl` and `mediaUrl` resolve a household media-library id, which is what [`field.image()`](/docs/widgets/widget-sdk/config) stores, to a URL. For Home Assistant `entity_picture` paths use the SDK's own `hassMediaUrl` instead. > **Warning:** Direct `@glasshome/ui` imports still work, and `bun widget build` warns once per file that uses one. They are removed in SDK 2.0.0, so migrate when you touch a widget: the import path is the only change. Missing a primitive? Ask for an SDK re-export instead of importing `@glasshome/ui`, so it arrives version-gated. ## Tailwind utilities Use Tailwind exactly as you would anywhere else: ```tsx 22°C ``` `tw-animate-css` is bundled too, so its animation utilities (`animate-in`, `fade-in`, `slide-in-from-bottom`, …) work without extra setup. ### Dark mode `dark:` variants work: the host mirrors the dashboard's `dark` class onto your shadow host element, and the SDK redefines the variant to be shadow-aware, so: ```tsx
``` reacts to the dashboard theme. For the boolean, use `isDark()` from the SDK. ## Adding your own CSS Import a stylesheet anywhere in your module graph and it joins the same `.css`, scoped to your shadow root: ```tsx // src/my-widget/index.tsx import "./styles.css"; ``` ```css /* src/my-widget/styles.css */ .spark { background: radial-gradient(circle, var(--widget-color), transparent); } @keyframes drift { from { transform: translateX(0); } to { transform: translateX(8px); } } ``` Plain CSS, nesting, custom properties, and `@keyframes` all work. Selectors match only your own DOM, so no defensive namespacing is needed. ## Theme variables The user's theme reaches your widget by **inheritance**: variables like `--background`, `--foreground`, `--card`, `--primary`, `--border`, `--muted`, `--accent`, and `--radius` are defined on the dashboard document and inherit across the shadow boundary, which is how a widget follows the household theme and live theme changes. Use them through Tailwind (`bg-card`, `text-foreground`, `rounded-[var(--radius)]`) or directly (`color: var(--foreground)`). > **Never redefine theme variables on :host:** Defining a theme variable on :host in your widget CSS freezes it at build-time and stops the value inheriting, so your widget stops reacting to the theme. The build fails if it finds one of the theme variables above defined under :host. Set your own (`--widget-*` or custom) variables freely; just leave the theme ones to inherit. Their **names** are part of the contract too. `var(--card)` is baked into your stylesheet at build time and resolved against the dashboard at mount, so renaming or dropping a token would break published widgets. The names are snapshotted and gated on our side: adding is routine, removing is deliberate. Build against the names above and they keep resolving. ## Responsive sizing with container queries There are **no size tiers and no JS measurement** for visual scale: the widget shell is a CSS size container (`container-type: size; container-name: widget`), and the SDK's own padding, gap, icon, and text sizes are fluid functions of it, so two widgets at the same rendered box look identical and resizing is smooth. Scale your own content the same way; Tailwind's container-query variants resolve against the `widget` container: ```tsx {time()} ``` Or write the query by hand in your CSS: ```css @container widget (min-aspect-ratio: 1) and (max-height: 149px) { .my-layout { flex-direction: row; } } ``` To branch *rendered content*, for example hiding a forecast strip on a small chip, read the measured box and apply your own pixel thresholds: ```tsx const dims = useWidgetDimensions(); const compact = () => dims().width < 200; ``` (`ctx.dimensions()` is deprecated since SDK 1.9 and removed in 2.0; `useWidgetDimensions()` throws outside `` instead of silently reporting a zero-size box. See the [API Reference](/docs/widgets/widget-api-reference).) ## Variant and shell The `variant` prop on `` picks the base shell look (background, blur, padding, layout defaults): `"classic-glass"` (the standard glass look of every official widget), `"minimal"`, or `"compact-horizontal"`; omit it for a bare, unstyled shell. The color props below layer on top. See [Variants](/docs/widgets/widget-sdk#variants) for the full list and helpers. ## Coloring the shell `` draws a gradient shell from a single accent color. Set it with props: | Prop | Effect | | ---------- | ---------------------------------------------------------------------------------------------------------------------- | | `tone` | Semantic color: `"success" \| "warning" \| "danger" \| "info" \| "neutral" \| "accent"`. Resolves to `--widget-color`. | | `color` | CSS color override for `--widget-color`. Overrides `tone`. | | `colorTo` | Second gradient stop (`--widget-color-to`). | | `gradient` | A full CSS gradient string; replaces the auto-generated shell. | ```tsx ``` ### SDK CSS custom properties Defined by the SDK on the widget shell; read them, and override except where noted: | Variable | Meaning | | ------------------------ | ----------------------------------------------------------------------------- | | `--widget-color` | The shell's accent color (driven by `tone`/`color`). | | `--widget-color-to` | Second gradient stop. | | `--widget-icon-color` | Per-icon / per-fill accent (set by `Widget.Icon color` / `SliderFill color`). | | `--widget-glow-strength` | Glow intensity multiplier for icons and fills. | | `--widget-fill-value` | Slider fill level, `0`–`100`. | | `--widget-radius` | Corner radius. Shell, content and fill share it (SDK 1.14.2+). | | `--tone-{name}` | The resolved color for each semantic tone. | The fluid scale tokens (`--widget-pad`, `--widget-gap`, `--widget-icon-box`, `--widget-title-size`, `--widget-value-size`, …) are also on the shell; override them on `.glasshome-widget` for a denser or looser look. ### SDK shell classes The `Widget` slot components render with these classes; target them to restyle a slot rather than replace it: | Class | Slot | | ------------------------------- | ------------------------------------------------------------ | | `.glasshome-widget` | The shell (gradient, border, container context) | | `.glasshome-widget-content` | `Widget.Content` layout wrapper | | `.glasshome-widget-icon` | `Widget.Icon` tile | | `.glasshome-widget-title` | `Widget.Title` | | `.glasshome-widget-status` | `Widget.Status` | | `.glasshome-widget-value` | `Widget.Value` | | `.glasshome-widget-subtitle` | `Widget.Value`'s interpretation line (`interpret`) | | `.glasshome-widget-badge` | `Widget.Title`'s badge | | `.glasshome-widget-icon-glyph` | The glyph inside `Widget.Icon` | | `.glasshome-widget-icon-stack` | Stacked layers behind `Widget.Icon` at `entityCount` 2 and 3 | | `.glasshome-widget-slider-fill` | `Widget.SliderFill` overlay | | `.glasshome-widget-loading` | The `loading` overlay | Need the SDK tokens in a root the host didn't set up (a custom mount, a test)? Call `injectTokens(root)`. ## Animation Standard CSS and Tailwind animation work inside the shadow root. SDK pieces that keep motion correct and cheap: - **`tw-animate-css` utilities** for one-shot enter/exit animations (`animate-in fade-in`, etc.). - **`loading` prop** on `` renders a pulsing overlay while data is pending: ``. - **`Widget.SliderFill`** animates a 0–100 fill (brightness, volume, …). Pass `isDragging` to drop the transition while the user drags so the fill tracks the finger 1:1. ```tsx ``` ### Respect reduced motion `useReducedMotion()` reactively tracks the user's `prefers-reduced-motion` setting; gate non-essential animation on it: ```tsx const reduced = useReducedMotion();
``` ### Pause off-screen animation `useIntersectionPause(el)` returns `true` while the element is outside the viewport, so you can stop animating widgets the user can't see: ```tsx let ref: HTMLDivElement | undefined; const paused = useIntersectionPause(() => ref); createEffect(() => { if (paused()) stopTicker(); else startTicker(); }); ``` ## Charts For SVG charts, `svgColors` gives energy-domain colors ready for `fill`/`stroke`. Each key (`solar`, `grid`, `battery`, `ev`, `home`, `positive`, `negative`) exposes `solid`, `stroke`, and a reduced-opacity `fill` for area shading: ```tsx import { svgColors } from "@glasshome/widget-sdk"; ``` For smooth lines, `monotoneCubicPath(points)` returns an SVG path string from a list of points. ## See also ### [Widget SDK guide](/docs/widgets/widget-sdk) Components, hooks, props, and the data layer. ### [API Reference](/docs/widgets/widget-api-reference) Every public export across the SDK, /schemas, and /vite. ### [Getting Started](/docs/widgets/widget-development) Scaffold, develop, and connect to a running dashboard. --- title: "Widget Previews" description: "Show people what your widget looks like before they install it. Declare examples once and Hub keeps the images up to date." slug: widgets/widget-previews canonical: https://glasshome.app/docs/widgets/widget-previews section: "Build Widgets" updated: 2026-07-29 --- # Widget Previews Someone browsing for a widget sees its name, its icon and a sentence. That is not much to decide on. Previews replace that with pictures of your widget doing its job, on the Hub page and in the dashboard's widget browser, before anyone installs it. You write a short list of showcase states. Everything else happens for you: each one is pictured in both light and dark, and the images refresh every time you publish a new version. There is nothing to upload and nothing to keep in sync. ## Declare your examples Add `examples` to your manifest in `defineWidget`. Each entry is a config to render, the size to render it at, and a label for your own reference. ```ts export default defineWidget({ manifest: { name: "Light", minSize: { w: 1, h: 1 }, maxSize: { w: 4, h: 4 }, sdkVersion: "^1.7.0", examples: [ { label: "Single light", size: { w: 2, h: 2 }, config: { title: "Reading Lamp", entityIds: ["light.bedroom_ceiling"] }, }, { label: "Room group", size: { w: 3, h: 2 }, config: { title: "Living Room", entityIds: ["light.living_room_main", "light.kitchen_counter"], }, }, ], }, configSchema, component: LightWidget, }); ``` - **`config`** is a real config for your widget, the same shape a user would end up with. It has to satisfy your `configSchema`. - **`size`** is in grid tiles, the same units as `minSize` and `maxSize`. Picture each state at the size it actually looks good. - **`label`** is optional and for your own orientation while writing them. The first example is the one used as your widget's thumbnail, so lead with the state that best represents it. ## Pick your best few Three or four is usually right. You are showing range, not documenting every option: the states worth picturing are the ones that look meaningfully different from each other. > **Info:** Up to 20 examples are pictured. If you declare more, the extras are not > rendered, so keep the list to the states you actually want people to see. ## Use the demo home Your examples reference entities from GlassHome's demo home, so they render with sensible data instead of empty states. Some of what is available: | Domain | Examples | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `light` | `light.bedroom_ceiling`, `light.living_room_main`, `light.kitchen_counter`, `light.hallway`, `light.bathroom`, `light.studio_rgb`, `light.desk_rgb` | | `climate` | `climate.living_room_thermostat`, `climate.bedroom_ac` | | `cover` | `cover.garage_door`, `cover.kitchen_blinds`, `cover.living_room_blinds`, `cover.bedroom_curtains` | | `lock` | `lock.front_door_lock`, `lock.back_door_lock` | | `switch` | `switch.coffee_machine`, `switch.fan_living_room` | | `media_player` | `media_player.living_room_speaker` | There are around 50 in total, including a generous set of `sensor` entities for readouts and graphs. The demo home is the same one the dashboard's demo mode uses, so anything you can build against there works here. `light.studio_rgb` and `light.desk_rgb` are colour-capable, which is handy if your widget has anything to show off in colour. ## See them before you publish Render your examples locally and look at them: ```bash bun widget preview ``` Images land in `preview/`, named `-