Prebuilt web frontend for the ESPHome Device Builder Dashboard
Project description
ESPHome Device Builder Dashboard — Frontend
A web-based dashboard for managing, configuring, and deploying ESPHome IoT device firmware. Built with Lit web components and TypeScript.
This repository contains the frontend source only. The dashboard runs as part of the ESPHome Device Builder Dashboard, which ships a prebuilt copy of this frontend bundled in. End users should follow the install / run instructions in the backend repo — there's nothing to deploy from here on its own.
Screenshots
Configured devices in the table view, with the discovered-devices banner above:
Discovered devices expanded — each card surfaces the project metadata and offers a one-click "Take control" adoption flow:
Create-device wizard's board picker — searchable, filterable by chip family, with curated featured boards up front:
Tech stack
- Lit — Web components framework
- TypeScript — Strict mode throughout
- Rspack — Rust-based bundler
- Web Awesome — UI component library (Home Assistant variant)
- CodeMirror — YAML editor with syntax highlighting
- Sonner — Toast notifications
Backlog
Before filing anything, take a look at the shared backlog — it lists everything that's already planned, in progress, or shipped for the dashboard. Saves duplicates and gives you a feel for where the project is heading.
Issues and feature requests
The new-issue chooser on this repo only surfaces redirect links — there's no way to file a generic issue here.
- 🐛 Bugs → backend issue tracker. UI bugs go there too so we can triage everything in one place.
- 💡 Feature ideas → ESPHome org discussions or the dashboard Discord channel where the new UI is actively discussed and feedback is being collected. Once a request is shaped enough to be actionable a maintainer adds it to the backlog above.
Contributing — local development
The rest of this README is for developers working on the frontend itself. If you just want to run the dashboard, head over to the backend repo and follow its setup.
Prerequisites
- Node.js 22+ (with npm)
- A running ESPHome Device builder backend on
localhost:6052— clone and run device-builder in dev mode in a separate terminal
Install
npm install
Dev server
npm run dev
Starts an HMR dev server at http://localhost:5173. WebSocket and REST traffic are proxied to the backend at localhost:6052.
Production build
npm run build
Outputs the bundled assets into esphome_device_builder_frontend/ — that directory doubles as the Python package source for the wheel that ships with the backend release. The __init__.py exposing the asset path is sourced from public/__init__.py and copied into place by the bundler.
To produce the wheel locally (matches what CI builds on release):
npm run build
python3 -m build --wheel
# wheel ends up in dist/
Other scripts
| Script | Description |
|---|---|
npm run lint |
TypeScript type-check (tsc --noEmit) |
npm test |
Run the Vitest suite once |
npm run test:watch |
Run tests in watch mode |
npm run format |
Format src/ with Prettier |
Translations
Contributing translations
Translations are crowd-sourced through Lokalise — no coding or GitHub account required. To help translate the dashboard into your language:
- Open the project's public signup link and join: https://app.lokalise.com/public/974668436a17ffd6803f51.79180045/
- Pick your language and start translating. Only native speakers should submit translations; proofreading existing work is just as valuable, even for a language that looks "complete".
- Don't translate proper nouns like
ESPHome,ESPHome Device Builder, orHome Assistant, and leave{placeholder}tokens (e.g.{count}) exactly as they are — the words around a token can be reordered, but the token itself must stay verbatim.
Don't see your language? It has to be added to the project once before it shows up — request it in the Discord language thread and someone will add it. Translated strings ship with the next release; any key a translator hasn't filled in yet falls back to English automatically.
How it works
src/translations/en.json is the English source of truth and the only translation file committed to the repo. Every other locale lives in Lokalise; those files are gitignored and pulled on demand with a small TypeScript tool at build-scripts/translations.ts (run directly by Node — no build step). That direct .ts execution relies on Node's native type stripping, so it needs Node 22.18+ (CI runs Node 24); on older Node it fails with ERR_UNKNOWN_FILE_EXTENSION.
# Push new/changed English keys up to Lokalise as the base language.
npm run translations:upload
# Pull the latest translations into src/translations/ (for local dev).
npm run translations:download
# Pull the locales from the latest GitHub release instead of Lokalise
# (no Lokalise token needed — reads the release's translations.zip asset).
npm run translations:download -- --source release
Both Lokalise commands read LOKALISE_API_TOKEN and LOKALISE_PROJECT_ID from the environment. upload only adds keys — it never overwrites translator edits; pass npm run translations:upload -- --cleanup to also delete Lokalise keys that no longer exist in en.json. download pulls every language the project has, writes each one except English (canonicalizing Lokalise's underscore ISO codes to the repo's BCP 47 filenames, e.g. zh_CN → zh-CN.json), and omits untranslated keys so the runtime English fallback in localize.ts stays in effect. A Lokalise download that returns no locales is treated as an error rather than silently shipping English-only. The --source release variant needs no Lokalise token (optionally GITHUB_TOKEN / GITHUB_REPOSITORY to raise rate limits or point at a fork) and reproduces exactly the locales the latest release shipped.
The frontend loader (src/common/localize.ts) is data-driven with no hardcoded locale list. The full message bodies load lazily: import.meta.webpackContext(..., { mode: "lazy" }) makes rspack emit one async chunk per locale, fetched only when that locale is selected, so English-only users download no other locale's strings (the English base is statically bundled as the always-present fallback). The language picker, however, needs each locale's autonym + flag synchronously up front — so a tiny src/generated/language-manifest.json (only those two keys per locale) is generated from src/translations/*.json at build time by build-scripts/gen-language-manifest.cjs and statically imported. That manifest is gitignored and regenerated automatically by build / dev / lint / the test global-setup; run npm run gen:languages to refresh it by hand. A checkout with no download (or a build before the secrets are set) simply ships English-only; the missing files are not an error. Locale codes are matched separator- and case-insensitively, so Lokalise's fr / zh_CN filenames resolve against the browser's BCP 47 fr-FR / zh-CN tags without any per-locale mapping.
In CI this is automated by:
translations-upload.yml— pushes to Lokalise wheneveren.jsonlands onmain.release.yml— downloads translations before bundling so the released wheel ships every locale.
There's no scheduled download workflow: because the locale files are gitignored, there's nothing to commit back to main. The translations only need to exist at build time, and release.yml pulls them fresh for the wheel; locally, run npm run translations:download whenever you want to work against the latest copy.
The workflows need the LOKALISE_API_TOKEN and LOKALISE_PROJECT_ID repository secrets. Adding a new locale is just a Lokalise change — translate it there and the next download picks it up automatically. The language picker is fully data-driven: each translation file carries its own display name and flag in the top-level language (autonym, e.g. Français) and flag (emoji, e.g. 🇫🇷) keys, so a new locale shows up named and flagged with no code change. If those keys are missing for a locale, the picker falls back to the raw code and a placeholder flag.
Thanks to Lokalise
Translations are managed with Lokalise, who generously provide their localization platform to the project free of charge. Thank you for supporting open source and helping bring the dashboard to more people in their own language.
Project structure
src/
├── api/ # WebSocket/HTTP API client and types
├── components/ # Lit web components
│ ├── device/ # Device editor, navigator, component catalog
│ └── wizard/ # Device creation wizard steps
├── pages/ # Routed page components (dashboard, device, secrets)
├── context/ # Lit Context definitions
├── common/ # i18n / localization
├── util/ # Helpers (debounce, YAML parsing, icons, ...)
├── styles/ # Theme and shared styles
├── translations/ # Language files (only en.json committed; rest from Lokalise)
└── entrypoint.ts # App bootstrap
public/
├── __init__.py # Python package entry — copied into the build
│ # output at bundle time so the wheel exposes a
│ # `where()` helper pointing at the static assets.
├── index.html # HTML shell
└── static/ # Static assets (favicons, ...)
esphome_device_builder_frontend/ # Build output (gitignored)
Code structure policies
These rules apply to all new code in src/. Existing files that pre-date them are grandfathered, but please don't make them worse.
File size
- Hard limit: 500–600 lines per file. Split before a file grows past this.
- No exceptions for "it's just one big component". Break it up.
Component decomposition
- Prefer many small, focused components over one large one.
- If a
render()method exceeds ~100 lines, that's a signal to extract a sub-component. - Extract repeated template patterns into their own components immediately — don't wait for the third copy.
Folder structure
- One
@customElementper.tsfile. File name matches element name:esphome-foo-bar.ts→<esphome-foo-bar>. - If a feature grows beyond 2–3 files, give it its own subfolder (see
src/components/settings-dialog/for the pattern). - Create folders proactively when grouping related files makes sense — don't pile everything flat.
TypeScript
strict: trueeverywhere. No implicitany, no non-null assertions without a clear reason.- New code uses
unknownand narrows; avoidany.
What to avoid
- No
document.querySelector— go through shadow DOM. - No direct DOM mutation — use reactive properties and re-render.
- No business logic in
render()— extract to private methods or computed properties. - No new global singletons for state two components need — use Lit context.
Localization
- All user-facing strings go through
_localize(key)fromsrc/common/localize.ts. - Add new keys to
en.jsononly — it's the source of truth and the sole committed locale. Translations are managed in Lokalise (see Translations), not hand-edited in the PR:translations:uploadpushes the new keys, translators fill them in, andtranslations:downloadbrings them back. Until then the i18n machinery falls back to English for the missing keys.
Comments
- Default comments. Add one only when the why is non-obvious (a constraint, an invariant, a workaround). Don't restate what the code does.
Releases
Releases are produced by GitHub Actions:
release.yml— manual trigger (or called fromauto-release.yml). Tags the version, drafts release notes from PR labels, builds the Python wheel, attaches it to the GitHub release, then opens or updates a single bump PR on the backend repo so it can pick up the new wheel URL.auto-release.yml— nightly cron that auto-releases when ≥ 2 commits have landed since the last release.dependabot.yml+auto-approve-dependabot.yml— weekly npm + Actions bumps with auto-approve.
The backend's pyproject.toml references the wheel by GitHub release URL (no PyPI), so a release here is everything needed to ship a new dashboard build.
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file esphome_device_builder_frontend-0.1.118.tar.gz.
File metadata
- Download URL: esphome_device_builder_frontend-0.1.118.tar.gz
- Upload date:
- Size: 1.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e265d02ffdd517ba8ae19395f33ce8af39d06b098f296fec81e50cbf7d3a5650
|
|
| MD5 |
acbf96549ffa730076b54ac3bcad5ac9
|
|
| BLAKE2b-256 |
b96e30c9072daaab68477317f137ca72d5850354ce94f80a298892de9b98f4eb
|
Provenance
The following attestation bundles were made for esphome_device_builder_frontend-0.1.118.tar.gz:
Publisher:
release.yml on esphome/device-builder-frontend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esphome_device_builder_frontend-0.1.118.tar.gz -
Subject digest:
e265d02ffdd517ba8ae19395f33ce8af39d06b098f296fec81e50cbf7d3a5650 - Sigstore transparency entry: 1706910359
- Sigstore integration time:
-
Permalink:
esphome/device-builder-frontend@e05542ac638dab6a30425310c6a76084f1872d3f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/esphome
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e05542ac638dab6a30425310c6a76084f1872d3f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file esphome_device_builder_frontend-0.1.118-py3-none-any.whl.
File metadata
- Download URL: esphome_device_builder_frontend-0.1.118-py3-none-any.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4592ad941f1dc2e0355b9b2b8ec73772115c3cc02c94d5296593e3f5bd89cb4
|
|
| MD5 |
1b61acdc32b1107da6686ae6b45f6129
|
|
| BLAKE2b-256 |
b2e82d2ad27b0ee6d77a9178f90fc11e8c8e234bd3d8283b37905ba2b8e3dfd0
|
Provenance
The following attestation bundles were made for esphome_device_builder_frontend-0.1.118-py3-none-any.whl:
Publisher:
release.yml on esphome/device-builder-frontend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esphome_device_builder_frontend-0.1.118-py3-none-any.whl -
Subject digest:
b4592ad941f1dc2e0355b9b2b8ec73772115c3cc02c94d5296593e3f5bd89cb4 - Sigstore transparency entry: 1706910404
- Sigstore integration time:
-
Permalink:
esphome/device-builder-frontend@e05542ac638dab6a30425310c6a76084f1872d3f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/esphome
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e05542ac638dab6a30425310c6a76084f1872d3f -
Trigger Event:
workflow_dispatch
-
Statement type: