Beta — This card is in active development. Features and configuration may change before the 1.0.0 stable release.
A customisable button card for Home Assistant focused on entity status. State-based icons, colors, labels, and animations give you at-a-glance visibility of your devices.
- State-based icon, color, and label per entity
- Built-in domain defaults for lock, light, switch, cover, climate, media player, alarm, and more
- Transitional state animations (e.g. locking/unlocking blink)
- Secondary entity support (e.g. door sensor paired with a lock)
- Custom state appearance overrides — pick from the entity's known states (or
secondary:prefix for the secondary entity) via a dropdown, with free-text fallback for template values - Configurable tap, double-tap, and hold actions via Home Assistant's native action editor
- Camera reveal — slide cameras down from the button when a configured state is active, with sync slide animation, configurable aspect ratio, fit mode (cover/contain), and position per camera
- Visual editor built on Home Assistant's
ha-formschema — matches the look and feel of HA's built-in card editors
- Open HACS in your Home Assistant instance
- Go to Frontend > + Explore & Download Repositories
- Search for Status Button Card by akaDashe
- Click Download
- Restart Home Assistant
- Download
status-button-card.jsfrom the latest release - Copy it to
config/www/status-button-card/ - Add the resource in Settings > Dashboards > Resources:
- URL:
/local/status-button-card/status-button-card.js - Type: JavaScript Module
- URL:
Add the card via the UI editor. The editor uses Home Assistant's ha-form schema, so it inherits HA's native styling, lazy element loading, and accessibility behaviour. Layout: top-level Entity + Secondary Entity, collapsible Appearance and Actions sections, plus a State Overrides list where each override expands to its own form (state value as a dropdown, icon, label, color, blink toggle, and an optional camera reveal list).
type: custom:status-button-card
entity: lock.front_doortype: custom:status-button-card
entity: lock.front_door
secondary_entity: binary_sensor.front_door_contact
name: Front Door
icon: mdi:door-closed-lock
show_name: true
show_label: true
active_color: "rgba(46, 175, 80, 0.9)"
inactive_color: "rgba(244, 67, 54, 1)"
transitional_color: "rgba(255, 170, 0, 1)"
state_appearances:
- state: locked
icon: mdi:lock
label: Locked
color: "rgba(46, 175, 80, 0.9)"
- state: unlocked
icon: mdi:lock-open
label: Unlocked
color: "rgba(244, 67, 54, 1)"
- state: locking
icon: mdi:lock-clock
label: Locking...
animate: true
- state: "secondary:on"
icon: mdi:door-open
label: Door Open
color: "rgba(255, 170, 0, 1)"
tap_action:
action: toggle
hold_action:
action: more-infoUse inside a horizontal-stack for a status bar:
type: horizontal-stack
cards:
- type: custom:status-button-card
entity: lock.front_door
- type: custom:status-button-card
entity: light.living_room
- type: custom:status-button-card
entity: alarm_control_panel.home
- type: custom:status-button-card
entity: cover.blindsNumeric comparison operators light up the right color band. Order matters — the highest-specificity bucket comes first, otherwise a 75°F reading would match > 65 before reaching > 75:
type: custom:status-button-card
entity: sensor.living_room_temperature
name: Living Room
state_appearances:
- state: ">= 80"
color: "rgba(244, 67, 54, 1)" # red
label: HOT
icon: mdi:thermometer-high
- state: ">= 75"
color: "rgba(255, 170, 0, 1)" # orange
label: WARM
icon: mdi:thermometer
- state: ">= 65"
color: "rgba(46, 175, 80, 0.9)" # green
label: COMFY
icon: mdi:thermometer
- state: "< 65"
color: "rgba(33, 150, 243, 1)" # blue
label: COLD
icon: mdi:snowflake
- state: "!= unavailable" # numeric ops won't match unavailable; this is a safety net
icon: mdi:help-circle-outlineA single regex collapses every armed_* state into one rule. The bare disarmed and triggered fall through to their own rules:
type: custom:status-button-card
entity: alarm_control_panel.home
state_appearances:
- state: triggered
color: "rgba(244, 67, 54, 1)"
label: TRIGGERED
animate: true
- state: "~= ^armed" # armed_home, armed_away, armed_night, armed_vacation, armed_custom_bypass
color: "rgba(46, 175, 80, 0.9)"
icon: mdi:shield-check
label: ARMED
- state: "~= ^(arming|pending|disarming)"
color: "rgba(255, 170, 0, 1)"
animate: true
- state: disarmed
color: "rgba(244, 67, 54, 1)"
label: DISARMED
icon: mdi:shield-offLock paired with a motion sensor. The lock's appearance follows its primary state; the camera reveals when the secondary motion sensor fires, regardless of lock state:
type: custom:status-button-card
entity: lock.front_door
secondary_entity: binary_sensor.porch_motion
state_appearances:
- state: "secondary:on" # motion detected
cameras:
- entity: camera.porch
aspect_ratio: 16/9
object_fit: cover
- state: locked
icon: mdi:lock
color: "rgba(46, 175, 80, 0.9)"
- state: unlocked
icon: mdi:lock-open
color: "rgba(244, 67, 54, 1)"| Option | Type | Default | Description |
|---|---|---|---|
entity |
string | Required | Primary entity ID |
secondary_entity |
string | Optional secondary entity (e.g. door contact sensor) | |
name |
string | Entity name | Display name |
icon |
string | Domain default | Default icon (mdi:icon-name) |
show_name |
boolean | true |
Show the name label |
show_label |
boolean | true |
Show the state label |
active_color |
string | Domain default | Color when entity is active |
inactive_color |
string | Domain default | Color when entity is inactive |
transitional_color |
string | Domain default | Color during transitional states |
state_appearances |
list | Per-state icon/label/color/cameras overrides | |
tap_action |
object | more-info |
Action on tap |
double_tap_action |
object | none |
Action on double tap |
hold_action |
object | none |
Action on hold |
min_width |
string | 68px |
Minimum button width (YAML only) |
max_width |
string | none |
Maximum button width (YAML only) |
height |
string | 68px |
Button height (YAML only) |
icon_size |
string | 26px |
Icon size (YAML only) |
| Option | Type | Description |
|---|---|---|
state |
string | Pattern to match the entity's state (see State Patterns). Prefix with secondary: to match against the secondary entity |
icon |
string | Icon override for this state |
label |
string | Label override for this state |
color |
string | Color override for this state |
animate |
boolean | Enable blink animation for this state |
cameras |
list | Cameras to reveal when this state is active (see Camera Reveal) |
The state field accepts comparison operators in addition to exact matches. Useful for sensors with numeric values, "anything but unavailable" guards, or regex matches across whole state families:
| Pattern | Matches |
|---|---|
locked |
exact string match (default when no operator is given) |
= idle |
explicit equals — same as bare idle |
!= unavailable |
not equal |
> 2, >2 |
numeric greater-than (entity state coerced via Number()) |
>= 2 |
numeric greater-or-equal |
< 100, <= 50 |
numeric less-than / less-or-equal |
~= ^armed |
regex test against the state string |
secondary:>50 |
any operator works with the secondary: prefix |
Important
Order is priority. Rules are evaluated top-to-bottom and the first one that matches wins — all subsequent rules are ignored for that state. List more-specific patterns above broader ones.
For example, to color a sensor amber when above 30 and red when above 60, the > 60 rule must come before the > 30 rule. If you put > 30 first, a state of 75 matches it and you'll see amber, never red.
In the visual editor, drag the grip handle on the left of each row to reorder. In YAML, the list order is the priority order.
Other notes:
- Numeric operators against non-numeric states (
unavailable,unknown,on) never match — they fall through to the next rule. - An invalid regex returns false rather than throwing — the rule simply doesn't match.
state_appearances:
- state: "!= unavailable" # primary entity reachable
icon: mdi:thermometer
- state: "> 30" # hot
color: rgba(244, 67, 54, 1)
label: "HOT"
- state: "~= ^armed" # any armed_* state
icon: mdi:shield-checkWhen a state appearance has a non-empty cameras list, that list of cameras slides down underneath the button while the state is active. The reveal slides back up automatically when the state changes. A small triangular pointer in the active state's color anchors the reveal to the button that triggered it.
Each camera entry can be a string (just the entity ID) or an object with extra options:
| Option | Type | Default | Description |
|---|---|---|---|
entity |
string | Required | Camera entity ID. Use a streaming-capable entity (e.g. from HA's native Ring integration) for live video — ha-camera-stream is rendered automatically when the entity supports streaming |
aspect_ratio |
string | 16 / 9 |
Frame aspect ratio for this camera (e.g. 16 / 9, 9 / 16, 4 / 3) |
object_fit |
cover | contain |
cover |
cover crops the video to fill the frame, contain letterboxes it |
object_position |
string | center |
Where the video sits inside the frame when its aspect doesn't match the frame's. One of center, top, bottom, left, right |
Example — alarm panel that reveals two cameras when armed:
type: custom:status-button-card
entity: alarm_control_panel.house
state_appearances:
- state: armed_home
icon: mdi:shield-home
cameras:
- entity: camera.front_door
- entity: camera.driveway
- state: armed_away
icon: mdi:shield-lock
cameras:
- entity: camera.front_door
aspect_ratio: 4 / 3
object_fit: cover
object_position: bottom
- entity: camera.driveway| Option | Type | Description |
|---|---|---|
action |
string | more-info, toggle, navigate, call-service, perform-action, url, none |
navigation_path |
string | Path for navigate action |
url_path |
string | URL for url action |
service |
string | Service for call-service action (e.g. lock.unlock) |
service_data |
object | Data to pass to service call |
The card includes built-in icon and color defaults for: light, switch, climate, media_player, cover, lock, camera, sensor, binary_sensor, fan, vacuum, person, device_tracker, alarm_control_panel, input_boolean, weather.
npm install
npm run build # Production build
npm run watch # Development with auto-rebuild
npm test # Run tests
npm run test:watch # Watch mode
npm run lint # ESLint
npm run format # PrettierIf you mount your HA config at /Volumes/config (e.g. via SMB), npm run push builds, deploys, and busts every cache layer:
npm run deploy # build + copy to /Volumes/config/www/community/status-button-card/
npm run dev # rollup watch + auto-deploy on every save
npm run bump # point the Lovelace resource at the freshly hashed filename
npm run push # deploy && bump in one goThe deploy emits two files: the canonical status-button-card.js (which HACS validates against) and a content-hashed status-button-card.<hash>.js. The bump script reads dist/.deploy-hash and points the Lovelace resource URL at the hashed filename, so every build produces a brand-new URL that no browser cache, HA service worker, or Lovelace resource list can serve stale. Old hashed siblings are pruned on each deploy.
bump-resource.py reads HA_URL and HA_TOKEN from <repo>/../.env. Override the resource match with --name foo or RESOURCE_NAME=foo. If dist/.deploy-hash is absent, it falls back to bumping a ?hacstag= query.