A community-driven boilerplate of best practices for Home Assistant Lovelace custom cards.
This project is a fully-featured starting point for building your own custom Lovelace card. It is built on modern tooling (Lit 3, TypeScript 5, Rollup 4) and demonstrates real-world patterns used in production HA custom cards:
- Visual editor with collapsible accordion sections
- Multiple entity support
- Conditional card visibility based on entity states
- Custom attribute display with label and unit overrides
- Multiple layout modes (vertical, horizontal) and display modes (card, badge)
- Comprehensive tap / hold / double-tap action support with repeat and haptic feedback
- Skeleton loading state while
hassinitialises - Responsive design and theme-aware CSS custom properties
- Open HACS in your Home Assistant instance.
- Go to Frontend → + Explore & Download Repositories.
- Search for Boilerplate Card and click Download.
- Refresh your browser.
- Download
boilerplate-card.jsfrom the latest release. - Copy it to
<config>/www/boilerplate-card.js. - Add a resource entry in your dashboard settings:
resources:
- url: /local/boilerplate-card.js
type: moduletype: custom:boilerplate-card
entity: light.living_roomtype: custom:boilerplate-card
entity: light.living_room
name: Living Room
icon: mdi:ceiling-light
layout: vertical
display_mode: card
card_style: default
accent_color: [255, 152, 0]
attribute_limit: 3
show_timestamps: true
tap_action:
action: toggle
hold_action:
action: more-info
double_tap_action:
action: navigate
navigation_path: /lovelace/lights| Name | Type | Required | Description | Default |
|---|---|---|---|---|
type |
string | Required | custom:boilerplate-card |
|
entity |
string | Optional | Primary HA entity ID | none |
name |
string | Optional | Card title override | Entity friendly name |
icon |
string | Optional | MDI icon override (e.g. mdi:lightbulb) |
Domain default |
area |
string | Optional | HA area to associate with the card | none |
show_error |
boolean | Optional | Render the error card template (for testing) | false |
show_warning |
boolean | Optional | Render the warning banner template (for testing) | false |
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
tap_action |
object | Optional | Action on single tap | action: more-info |
hold_action |
object | Optional | Action on 500 ms hold | action: none |
double_tap_action |
object | Optional | Action on double tap | action: none |
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
action |
string | Required | more-info toggle navigate url call-service fire-dom-event none |
more-info |
navigation_path |
string | Optional | Path for navigate (e.g. /lovelace/0/) |
none |
url_path |
string | Optional | URL for url action — opens in a new tab |
none |
service |
string | Optional | Service for call-service (e.g. light.turn_on) |
none |
service_data |
object | Optional | Service data payload for call-service |
none |
haptic |
string | Optional | Haptic feedback: success warning failure light medium heavy selection |
none |
repeat |
number | Optional | Repeat interval in ms while held (hold_action only) |
none |
repeat_limit |
number | Optional | Maximum number of repeats (hold_action only) |
none |
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
layout |
string | Optional | vertical (stacked) or horizontal (icon + info + actions in one row) |
vertical |
display_mode |
string | Optional | card (full ha-card) or badge (compact inline chip) |
card |
card_style |
string | Optional | default compact detailed minimal |
default |
accent_color |
[r, g, b] | Optional | RGB accent color applied via --card-accent-color |
Theme primary color |
| Value | Effect |
|---|---|
default |
Standard padding and font sizes |
compact |
Reduced padding and smaller text |
detailed |
Enlarged text, bigger icon, extra padding |
minimal |
Entity row only — attributes and timestamps hidden |
| Name | Type | Required | Description | Default |
|---|---|---|---|---|
attribute_limit |
number | Optional | Maximum number of attributes to display (0 = none). Shown as a slider (0–10) in the visual editor. |
3 |
show_timestamps |
boolean | Optional | Show last changed / last updated timestamps | true |
| Tool | Minimum version | Notes |
|---|---|---|
| Node.js | 24 | Required by custom-card-helpers@2 |
| Yarn | 4 | Managed via Corepack |
TypeScript, Rollup, ESLint, and all other build tools are installed locally via yarn install — no global installs needed.
The devcontainer gives you a full HA development environment in one click with no local setup required.
- Open the project in VS Code.
- When prompted, click Reopen in Container (or run Dev Containers: Rebuild Container).
- A local Home Assistant instance starts automatically at
http://localhost:8123. - Log in with
dev/dev. - The built card is served from the container and hot-reloads on every save (
yarn startis launched automatically).
Requires: Dev Containers extension.
# 1. Clone or use the GitHub "Use this template" button
git clone https://github.com/custom-cards/boilerplate-card.git my-card
cd my-card
# 2. Install dependencies
yarn install
# 3. Verify the build works
yarn build
# 4. Start the development watcher
yarn startThen add your local file as a Lovelace resource:
resources:
- url: /local/boilerplate-card.js
type: moduleCopy or symlink dist/boilerplate-card.js into your HA www/ folder, or use the devcontainer where this is handled automatically.
| Command | Description |
|---|---|
yarn build |
Lint + production bundle (minified, ES2022 output) |
yarn rollup |
Production bundle only (skips lint) |
yarn start |
Development watcher with hot reload (rollup --watch) |
yarn lint |
ESLint across all src/ files |
src/
├── boilerplate-card.ts # Main card element — LitElement subclass
├── editor.ts # Visual editor — implements LovelaceCardEditor
├── types.ts # TypeScript interfaces for all config fields
├── const.ts # CARD_VERSION constant
├── action-handler-directive.ts # Lit directive: tap / hold / double-tap gestures
└── localize/
├── localize.ts # i18n helper
└── languages/
├── en.json # English strings
└── nb.json # Norwegian strings
dist/
└── boilerplate-card.js # Build output — serve this to HA
Search the codebase for TODO — every required change is marked. The key steps in order:
- Rename the element — change
boilerplate-cardeverywhere: the@customElementdecorator, thecustomCards.pushentry, the editor tag name ingetConfigElement, and your YAMLtype:field. - Define config fields — add your options to
BoilerplateCardConfigintypes.ts. - Validate and set defaults — update
setConfig()inboilerplate-card.ts. Throw for missing required fields; spread sensible defaults for optional ones. - Update
getStubConfig— return a minimal valid config so the card picker renders something immediately without the editor. - Build your render — replace
_renderContent()and_renderAttributes()with your domain-specific templates. - Update the visual editor — add your config fields to the relevant accordion section in
editor.ts. Useha-selectorfor type-safe inputs that match HA's UI conventions. - Add user-facing strings — put labels and messages in
src/localize/languages/en.jsonand reference them withlocalize('key').
| Tool | Version | Role |
|---|---|---|
| Lit | 3.2 | Web components framework |
| TypeScript | 5.6 | Type checking and compilation |
| Rollup | 4 | Bundler — single .js output with tree-shaking |
@rollup/plugin-typescript |
11 | TypeScript integration |
@rollup/plugin-terser |
0.4 | Minifier |
| ESLint | 8 | Linting with TypeScript and Prettier integration |
Important: Rollup and Terser are both configured for ES2022 output (
ecma: 2022,target: 'ES2022'). Do not lower these targets. Lit 3 uses native ES6classsyntax and theextendskeyword; downgrading to ES5 causes a runtimeTypeError: Class constructor cannot be invoked without 'new'.
action-handler-directive.ts is a Lit directive that attaches gesture recognisers to any element. Wire it up with the actionHandler() function and listen for the @action event:
import { actionHandler } from './action-handler-directive';
import { handleAction, ActionHandlerEvent } from 'custom-card-helpers';
// In your render():
html`
<div
${actionHandler({ hasHold: true, hasDoubleClick: true })}
@action=${this._handleAction}
tabindex="0"
>...</div>
`
// Handler:
private _handleAction(ev: ActionHandlerEvent): void {
handleAction(this, this.hass, this.config, ev.detail.action);
}| Gesture | Activated by |
|---|---|
| Tap | Release before the hold threshold |
| Hold | Press held for 500 ms |
| Hold + repeat | Fires every repeat ms while held |
| Double tap | Two taps within 250 ms |
| Keyboard | Enter or Space triggers tap |
- Copy
src/localize/languages/en.jsontosrc/localize/languages/<lang>.json. - Translate the values (keep all keys identical).
- Import and register the new translations in
src/localize/localize.ts.
- Fork the repository and create a feature branch from
master. - Run
yarn buildbefore opening a PR — all lint checks must pass. - Keep PRs focused on a single change.
- Code style is enforced automatically by Prettier and ESLint on build.
Card not appearing after install
Clear your browser cache or do a hard reload (Ctrl+Shift+R / Cmd+Shift+R).
TypeError: Class constructor cannot be invoked without 'new'
Your bundler is transpiling Lit's class syntax down to ES5. Ensure rollup.config.js has terser({ ecma: 2020 }) and typescript({ compilerOptions: { target: 'ES2022' } }).
Visual editor not opening
Check the browser console for import errors from the dynamic import('./editor') in getConfigElement. Also confirm the boilerplate-card-editor custom element tag matches what getConfigElement creates.
Card stuck on skeleton / not rendering
shouldUpdate may be returning false before hass is ready. The safest implementation for a single entity is:
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (!this.config) return false;
return changedProps.has('config') || changedProps.has('hass');
}General Lovelace plugin troubleshooting See the thomasloven wiki.