Taming the Lovelace Dashboard: A Production Ops Guide to Home Assistant Views and Cards
1. The Dashboard Is the Front Line
Every smart home has a brain. Mine is a Home Assistant instance running 60+ integrations and 200+ entities — and, the part nobody talks about, a Lovelace dashboard my whole household actually uses. Automation logic matters, but the dashboard is the front line. When a view is wrong, a card is stale, or a button does nothing, the system loses trust in seconds.
I built this dashboard from scratch, iterating through three production incidents and dozens of config rewrites. This article is my field log: how I organize views, which cards I trust, how I template safely, and the five pitfalls that genuinely broke my UI.
2. Storage Mode vs YAML Mode
When you first enable the dashboard, Home Assistant asks you to pick: manage it in the UI (storage mode) or define it in configuration.yaml (YAML mode). I started in storage mode because it was fast and pretty. Then I made a change at 11 PM, broke a card, and realized I had no idea what I'd touched — the config lived in .storage/lovelace as a giant JSON blob with no version control.
For production, I moved to YAML mode:
# configuration.yaml
lovelace:
mode: yaml
resources:
- url: /local/community/card-mod/card-mod.js
type: module
- url: /local/community/lovelace-templated-card/templated-card.js
type: module
With YAML mode, every view and card is a file in Git. I review a diff before a change lands, roll back with git revert, and never lose a dashboard to a stray click. The trade-off is no drag-and-drop — I edit text, reload, and check.
3. Organizing Views That Survive Production
My dashboard has five views, each mapped to a room or a concern. The golden rule: one view = one mental model.
views:
- title: Overview
path: overview
icon: mdi:home
- title: Living Room
path: living-room
icon: mdi:sofa
- title: Climate
path: climate
icon: mdi:thermostat
- title: Energy
path: energy
icon: mdi:flash
- title: Media
path: media
icon: mdi:television
Three lessons from production:
- Set a
pathon every view. Default paths are hashes. When an entity_id changes and a card breaks, a readable path likeliving-roommakes debugging fast. - Use sections, not
panel: true. Panels are great for a single card but terrible for a growing dashboard. Sections (HA 2023.11+) let columns collapse gracefully on phones. - Keep views shallow. Five views with 3–8 cards each beats one view with 40 cards.
4. The Card Toolbox: Six Cards I Actually Use
After a year of iteration, six card types cover 90% of my dashboard.
entities — the workhorse. Groups entities with state, icon, and secondary info.
type: entities
title: Hallway Sensors
entities:
- entity: sensor.hallway_temperature
name: Temp
- entity: sensor.hallway_humidity
name: Humidity
- entity: binary_sensor.hallway_motion
name: Motion
glance — at-a-glance states with no controls. tile — my favorite; the card HA recommends for a button-like control with state, and it handles tap actions cleanly.
history — pulls from the recorder database. This is the one that bit me (Pitfall #3). weather — binds to a weather domain entity; one line, no config beyond entity: weather.home. media — ties to a media_player; shows artwork, transport controls, and volume.
The lesson: choose the card for the job. A tile for a switch, a history for a sensor trend, entities for a status cluster. Don't cram everything into one entities card.
5. Templating and card-mod: Power, With a Price
Lovelace cards are static by default. To make them dynamic I use two tools: card templating (via templated-card) and card-mod for styling.
card-mod can style anything, but it also silently ignores invalid CSS. Real snippet:
type: entities
entities:
- entity: sensor.garage_door_state
card_mod:
style: |
:host {
color: {{ 'green' if is_state('binary_sensor.garage_door', 'off') else 'red' }};
}
That Jinja inside style looks correct, but card-mod evaluates its style block through its own template engine, not always HA's. I spent an evening chasing a card that rendered fine but never updated, because I put a live is_state() somewhere card-mod evaluated only once.
The safer pattern is a template card that rebuilds its config:
type: custom:templated-card
template:
- "type: tile"
- "entity: light.hallway"
- >
{% if is_state('binary_sensor.hallway_motion', 'on') %}
icon: mdi:motion-sensor
{% else %}
icon: mdi:light-outline
{% endif %}
Rule of thumb: put dynamic values in a template card, not in card-mod. card-mod is for static styling only.
6. Pitfall #1: YAML Syntax Traps
The first incident that took my dashboard down: a missing indent. One card's type: was nested one level too deep. HA didn't crash — it rendered the view with zero cards and logged a single warning.
The trap: YAML is whitespace-significant. A tab sneaks in (a paste from an email brought real tabs), and the parser treats a list as a scalar. I run every config change through home-assistant --script check_config plus a YAML linter in CI.
# Wrong: 'cards' is not a valid key on an entities card (silently dropped)
type: entities
entities:
- entity: sensor.office_temperature
cards:
- type: glance
entities: [sensor.office_temperature]
# Right
type: entities
entities:
- entity: sensor.office_temperature
card_mod:
style: |
:host { font-weight: bold; }
The entities card has no cards key. HA is lenient and just drops it.
7. Pitfall #2: Permissions and the Blank Dashboard
A user reported: "The dashboard is blank for my wife." Everything looked fine on my admin account. The cause was user permissions. My config referenced entities the non-admin user couldn't read, so the cards silently disappeared. No error, no warning — just empty space.
The fix: I created a "family" user group with read access to the relevant areas, and audited every entity: reference against that group. I now test the dashboard as a least-privilege user before shipping: log in as the restricted user, open every view, and confirm no blank tiles.
# Not every user sees everything. Confirm the user can read this entity.
- entity: sensor.garage_door_state
8. Pitfall #3: Stale Data and the Refresh Problem
The history card was the worst. A sensor updated every minute, but the history card showed a flat line from an hour ago. The cause: the history card queries the recorder, and the recorder's commit_interval (default 30s) plus the card's own cache made my "live" history minutes old. Some cards only refresh on visibility or a manual interval.
The fix was two-part:
For live tiles that must not lag, I use update_interval on a template card.
The lesson: know which layer is stale. Live state (tile/entities) is fast. History is a database query. Don't mix them on one view and expect them to agree.
9. Pitfall #4 & #5: Entity IDs and View Ordering
This one hurt. I renamed a switch from switch.garden_lights to switch.garden_lighting to fix a typo. Every card referencing the old ID went blank — and because one was a template card with a hardcoded entity:, it didn't even show an error; it just rendered nothing.
The fix: grep before and after every rename.
In YAML mode, I never hardcode an entity_id in ten places. I centralize card config, and for anything I might rename, I route through a helper so a rename is one edit, not ten.
The same fragility applies to view order. I once dragged a view in the UI (back when I was in storage mode) to "reorganize" it, and it jumped to index 0. Every user's default view changed. In production, the first view in the views list is the default. A single reorder changed what everyone saw on load.
Now I define views explicitly and protect the landing view:
views:
- title: Overview
path: overview
icon: mdi:home
# Index 0 = the landing view. Never reorder it casually.
The fix in YAML mode is trivial: order in the file equals order in the app.
10. A Complete Production Dashboard
Here is a condensed, production-tested config I actually run. It shows all the patterns above: YAML mode, sections, a mix of cards, templating, and card-mod for static styling.
lovelace:
mode: yaml
resources:
- url: /local/community/card-mod/card-mod.js
type: module
views:
- title: Overview
path: overview
icon: mdi:home
sections:
- type: grid
cards:
- type: tile
entity: light.kitchen_ceiling
tap_action:
action: toggle
- type: tile
entity: climate.living_room
tap_action:
action: more-info
- type: weather
entity: weather.home
- type: grid
cards:
- type: entities
title: Hallway Status
entities:
- entity: sensor.hallway_temperature
- entity: sensor.hallway_humidity
- entity: binary_sensor.hallway_motion
name: Motion
- type: history-graph
title: Hallway Temp (6h)
entities:
- entity: sensor.hallway_temperature
hours_to_show: 6
- type: grid
cards:
- type: custom:templated-card
template:
- "type: tile"
- "entity: light.hallway"
- >
{% if is_state('binary_sensor.hallway_motion', 'on') %}
icon: mdi:motion-sensor
{% else %}
icon: mdi:light-outline
{% endif %}
This is what survives a year of production. It's boring, predictable, and debuggable — exactly what a dashboard should be.