Components
Lit components on native elements, styled only through --gw-* variables, and kept current in each app by the sync.
The web components are Lit elements built on native elements: a gw-button is a <button>, a gw-dialog is a <dialog>, a gw-segmented is a set of radio buttons. Keyboard use, focus and forms come from the browser, and Home Assistant’s frontend is the reference for the rest. They’re styled only through --gw-* variables, so each follows whichever theme and mode surrounds it.
Every demo on these pages has a theme and mode picker. Pick one and the components follow it.
Groups
| Group | Components |
|---|---|
| Actions | Button, menu button, segmented control |
| Inputs | Input, select, checkbox, switch, setting row |
| Display | Card, chip, badge, dot, alert, facts, meter |
| Feedback | Spinner, skeleton, empty state, toast |
| Overlays | Popover, dialog, menus |
| Layout | Page, page header, section title, stack, row, grid |
| lit-effect | AtomController, the registry context and a typed router |
The data group (table, virtual list, time series chart and JSON view) comes next.
Taking them into an app
The sync brings components into an app and keeps them current. Add the ones an app uses once, and mise run sync updates them after every groundwork change:
bun packages/sync/src/cli.ts add --app ../my-app --to src/ui gw-button gw-popover themes-css
Each component needs ui-base (its shared styles and the icon() helper), which the sync brings along. Load themes.css and set data-gw-theme and data-gw-mode on a container, and everything inside follows. Extend a component by wrapping it or setting variables and ::part(), never by editing it in the app, since the sync flags those edits.
Fields stay in light DOM
Password managers and <label for> can’t reach into a shadow root, and a form only submits the controls it can see. So gw-input, gw-select, gw-checkbox and gw-switch render their native control in light DOM, and adopt their styles into whichever document or shadow root they’re in. Pages with credentials keep the whole form out of shadow roots.
Icons
icon(path, label?) draws any SVG path data, so each app picks its set: MDI (as Home Assistant uses), Lucide, Tabler or Phosphor. Components that show an icon take it as path data in an icon attribute, or an <svg slot="icon">.