Skip to content

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:

  1. Set a path on every view. Default paths are hashes. When an entity_id changes and a card breaks, a readable path like living-room makes debugging fast.
  2. 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.
  3. 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.

type: tile
entity: light.kitchen_ceiling
icon: mdi:ceiling-light
tap_action:
  action: toggle

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.

type: media-control
entity: media_player.living_room_tv

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:

# recorder config
recorder:
  commit_interval: 10

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.

grep -rn "switch.garden_lights" /config/

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.