Crafting My Perfect Home Assistant Dashboard

Bruno SabotBruno Sabot
11 min read

Inspired by the beautiful and functional dashboards I've seen online, I embarked on a journey to personalize my own Home Assistant experience. This post dives into the plugins I used, my customization choices, and a final glimpse of the finished product.

Introducing the Plugin Powerhouse

Several plugins played a pivotal role in building my dream dashboard, each offering unique functionalities that elevated the user experience.

I meticulously handpicked a select few from the vast plugin library, prioritizing consistency and cohesiveness around the versatile Bubble Card, to craft this personalized experience.

Bubble Card

bubble-card.png

I recently stumbled upon the Bubble Card plugin, and let me tell you, it was a game-changer!

This new plugin quickly became the star of my dashboard makeover. Its cool, rounded design elements totally revamped the whole look and feel, making everything smooth and visually appealing.

To take things to the next level, I also started using the Bubble theme, created by the same awesome creator.

Together, they work like magic, creating a consistent and stunning dashboard that's more than just the sum of its parts.

It's like my dashboard went from info central to an interactive and visually engaging masterpiece!

Card Mod

Anyone who's ever tinkered with their dashboard to make it look snazzy probably knows about Card Mod – it's kind of a no-brainer.

This awesome plugin lets you, well, mod your cards by taking styles from other cards. Need to change the background color, for example? Card Mod is your new best friend.

It helps if you know a little CSS, but even without it, you can do some pretty cool stuff and make your dashboard look way less, well, boring.

Decluttering Card

Card Mod is so cool, I went a little overboard and ended up customizing practically everything! But all those tweaks scattered throughout my config started to feel like a bit of a pain to manage.

That's when I discovered the magic of Decluttering Card. This plugin lets me create card templates that I can reuse everywhere, shrinking my configuration file down to a much more manageable size.

Now, I can make a change in one place, and it ripples through my entire dashboard – a total win!

Config Template Card

Decluttering Card definitely helps with the templating issue, but it can't handle everything, especially when things get a little more dynamic.

Thankfully, the Config Template Card swoops in to save the day! This plugin lets you use JavaScript to make pretty much any property of your card dynamic, putting the finishing touches on your template mastery.

It's like the final ingredient in the recipe for perfect dashboard customization.

Lovelace Layout Card

Now I have the power to create the perfect cards, I just needed the final touch: a stellar layout.

Thankfully, Lovelace Layout Card swoops in to save the day! This plugin allows me to craft a perfectly responsive dashboard that flawlessly adapts to both my desktop and mobile, ensuring a seamless Home Assistant experience no matter the device I'm using.

Bringing it to Life: My Customization Journey

In the following sections, I'll delve into the specifics of how I utilized these plugins to realize my desired dashboard functionalities.

I'll provide a detailed explanation for each, empowering you to replicate or adapt these approaches to craft your own personalized Home Assistant experience.

Alarm

alarm.png

Keeping tabs on my home alarm system's status is crucial. To achieve this, I needed a clear and immediate visual indicator right on my dashboard.

The ideal card would dynamically change its background color based on the alarm's state: green for disabled, orange for armed, and red for triggered. Luckily, the style customization of Bubble Card allowed me to create this functionality perfectly.

Now, with just a glance, I can see the alarm's status and take appropriate action if needed.

Here is the decluttering-card template:

default:
  - name: ''
  - columns: 2
card:
  type: custom:bubble-card
  card_type: button
  button_type: state
  entity: '[[entity]]'
  name: '[[name]]'
  show_state: true
  icon: mdi:alarm-light
  columns: '[[columns]]'
  card_layout: large
  styles: |
    .bubble-button-card-container {
      background-color: ${state === 'disarmed' ? 'var(--success-color)' : state === 'triggered' ? 'var(--error-color)' : 'var(--warning-color)'};
    }
    .bubble-icon {
      color: ${state === 'disarmed' ? 'var(--success-color)' : state === 'triggered' ? 'var(--error-color)' : 'var(--warning-color)'} !important;
      opacity: 0.6 !important;
    }

AQI

aqi.png

Monitoring air quality is a top priority for me, as I'm sensitive to pollution.

Having this data readily available on my dashboard helps me maintain my health, not only through automations with my air purifier but also by allowing me to check the current pollution level at a glance.

I envisioned a card that would display the Air Quality Index (AQI) using a color-coded system: green for good air quality (AQI below 50), orange for moderate air quality (AQI below 100), and red for poor air quality (AQI above 100).

This way, I could easily understand the air quality and take appropriate actions, if necessary.

Here is the decluttering-card template:

default:
  - name: ''
card:
  type: 'custom:bubble-card'
  card_type: button
  button_type: state
  entity: '[[entity]]'
  name: '[[name]]'
  icon: mdi:leaf
  show_state: true
  columns: 2
  card_layout: large
  styles: |
    .bubble-button-card-container {
      background-color: ${state < 50 ? 'var(--success-color)' : state < 100 ? 'var(--warning-color)' : 'var(--error-color)'};
    }
    .bubble-icon {
      color: ${state < 50 ? 'var(--success-color)' : state < 100 ? 'var(--warning-color)' : 'var(--error-color)'} !important;
      opacity: 0.6 !important;
    }

Covers

cover.png

My cover cards demanded a presentation that was both information-rich and visually clear. To achieve this, I implemented a three-pronged approach:

  • Condensed Icon Spacing & Favorite Position Integration: I tightened the spacing between the cover icons, fostering a sense of cohesion for the linked actions they represent. Additionally, I incorporated a sub-button displaying the favorite position (set at 15% in my case) directly within the card itself.

  • Seamless Opening Percentage Display: The current opening percentage is crucial information, and I eliminated the need for a separate entity card by integrating it directly into the cover card.

  • Color-Coded Status Recognition: Finally, I implemented a color-coding system to provide instant status recognition. Blue signifies a fully open cover, while purple highlights the preferred 15% position. This visual language allows for quick comprehension and informed decision-making when interacting with my covers.

This combined approach effectively streamlined the cover card format, enhancing both information accessibility and user experience.

Here is the decluttering-card template:

default:
  - name: ''
card:
  type: 'custom:bubble-card'
  card_type: cover
  entity: '[[entity]]'
  name: '[[name]]'
  icon_open: mdi:window-shutter-open
  icon_close: mdi:window-shutter
  show_state: true
  columns: 2
  card_layout: large
  sub_button:
    - name: My
      icon: mdi:star
      show_background: false
      tap_action:
        action: call-service
        service: button.press
        target:
          entity_id:
            - '[[my]]'
  card_mod:
    style: |
      .bubble-cover-card-container {
        background-color:
          {% if state_attr(config.entity, 'current_position') == 15 %}
            var(--state-cover-active-color)
          {% elif state_attr(config.entity, 'current_position') > 0 %}
            var(--accent-color)
          {% else %}
            var(--background-color-2, var(--secondary-background-color))
          {% endif %}
        !important;
      }
      .large .bubble-buttons {
        gap: 8px;
      }
      .bubble-button {
        background: transparent;
        width: 40px;
        height: 40px;
      }
      .bubble-icon {
        justify-content: center;
        align-items: center;
      }
      .large .bubble-sub-button-container {
        margin-right: 8px;
      }
      .bubble-sub-button {
        background: transparent;
        width: 40px;
        height: 40px;
      }
      .bubble-state::after {
        content: " - {{ state_attr(config.entity, 'current_position') }}%";
        margin-left: 4px;
      }
      .bubble-icon-container {
        border-color: transparent;
        background-color: transparent;
      }
      .bubble-icon-container::after {
        content: "";
        background-color:
          {% if state_attr(config.entity, 'current_position') == 15 %}
            var(--state-cover-active-color)
          {% elif state_attr(config.entity, 'current_position') > 0 %}
            var(--accent-color)
          {% else %}
            var(--background-color-2, var(--secondary-background-color))
          {% endif %}
        !important;
        height: 25px;
        width: 25px;
        position: absolute;
        left: 50px;
        top: 25px;
        -webkit-mask-image: radial-gradient(circle at top right, transparent 0, transparent 25px, black 25.5px);
      }

Separator

separator.png

To keep my dashboard organized, I grouped things into sections.

Bubble Card's separator mode was perfect for adding section titles, but I wanted to give them a little makeover.

So, I played around with some CSS to make the font lighter and bigger, and tweaked the line color so it stands out even when the cards aren't in a popup.

Now the section titles are easy to read and add a nice touch to the whole thing!

Here is the decluttering-card template:

default:
  - icon: ''
  - name: ''
card:
  type: custom:bubble-card
  card_type: separator
  name: '[[name]]'
  icon: '[[icon]]'
  card_layout: large
  styles: |
    .bubble-name {
      font-weight: 100;
      font-size: 20px;
    }
    .bubble-line {
      background-color: var(--background-color-2, var(--secondary-background-color));
      opacity: 0.2;
    }

Thermostat

thermostat.png

Craving more from my smart thermostat display, I used custom-template-card to dynamically change the card's icon based on heating state.

Color cues joined the party: blue for off, green for achieved comfort, and orange for heating to reach temperature. I even incorporated the target temperature in this customized switch card.

Now, it's a one-stop shop for efficient and comfortable climate control!

default:
  - name: ''
  - offset: 1
card:
  type: custom:config-template-card
  variables:
    MODE: "states['[[entity]]'].state"
  entities:
    - "[[entity]]"
  card: 
    type: custom:bubble-card
    card_type: button
    button_type: switch
    entity: '[[entity]]'
    name: '[[name]]'
    icon: "${MODE === 'heat' ? 'mdi:thermometer': 'mdi:thermometer-off'}"
    show_state: true
    columns: 2
    card_layout: large
    card_mod:
      style: |
        .bubble-button-card-container {
          background-color: {% if is_state(config.entity, 'heat') %}{% if state_attr(config.entity, 'temperature') > state_attr(config.entity, 'current_temperature') %}var(--warning-color){% else %}var(--success-color){% endif %}{% else %}var(--info-color){% endif %} !important;
        }
        .bubble-icon {
          color: {% if is_state(config.entity, 'heat') %}{% if state_attr(config.entity, 'temperature') > state_attr(config.entity, 'current_temperature') %}var(--warning-color){% else %}var(--success-color){% endif %}{% else %}var(--info-color){% endif %} !important;
          opacity: 0.6 !important;
        }
        .bubble-state::after {
          content: " - {{ state_attr(config.entity, 'temperature') }} °C";
          margin-left: 4px;
        }

Weather

weather.png

While Bubble Card forms the foundation, the true magic lies in the customization of my weather display. It leverages the power of sub-buttons to showcase a range of essential information: current conditions, daily highs and lows, wind speed, and the anticipated rain forecast.

But the key is the intuitive color-coding system I implemented:

  • Temperatures utilize a blue-to-red gradient, providing a quick glimpse of the expected highs and lows.

  • Wind speed follows a similar approach, transitioning from neutral to red as intensity increases.

  • Rain takes center stage with a prominent blue highlight whenever there's a chance of precipitation.

This visual language allows me to grasp the daily forecast at a glance, without getting bogged down in numbers. It's a testament to the power of visual communication and user experience design in action!

default:
  - name: ''
card:
  type: custom:bubble-card
  card_type: button
  button_type: state
  entity: '[[entity]]'
  name: '[[name]]'
  show_state: true
  scrolling_effect: false
  card_layout: large-2-rows
  sub_button:
    - name: Min
      icon: mdi:thermometer-low
      entity: '[[entity]]'
      attribute: forecast[0].templow
      show_background: false
      show_attribute: true
    - name: Wind
      icon: mdi:weather-windy
      entity: '[[entity]]'
      attribute: wind_speed
      show_background: false
      show_attribute: true
      show_icon: true
    - name: Max
      icon: mdi:thermometer-high
      entity: '[[entity]]'
      attribute: forecast[0].temperature
      show_background: false
      show_attribute: true
    - name: Rain
      icon: mdi:weather-pouring
      entity: '[[entity]]'
      show_background: false
      show_attribute: true
      attribute: forecast[0].precipitation
      show_icon: true
  card_mod:
    style: |
      .bubble-icon-container {
        background: transparent;
      }
      .bubble-name {
          border-radius: 20px;
          background-color:
          {% if state_attr(config.entity, 'temperature') < 10 %}
            var(--info-color)
          {% elif state_attr(config.entity, 'temperature') > 40 %}
            var(--error-color)
          {% elif state_attr(config.entity, 'temperature') > 30 %}
            var(--warning-color)
          {% endif %}
          ;
          margin-left: -8px;
          padding: 0 8px;
      }
      .bubble-name::after {
          content: " - {{ state_attr(config.sub_button[0].entity, 'temperature')  }}°C";
          margin-left: 4px;
      }
      .bubble-sub-button-1 {
          background-color:
          {% if state_attr(config.sub_button[0].entity, 'forecast')[0].templow < 10 %}
            var(--info-color)
          {% elif state_attr(config.sub_button[0].entity, 'forecast')[0].templow > 40 %}
            var(--error-color)
          {% elif state_attr(config.sub_button[0].entity, 'forecast')[0].templow > 30 %}
            var(--warning-color)
          {% endif %}
          ;
      }
      .bubble-sub-button-1::before {
        content: "°C";
      }
      .bubble-sub-button-2 {
          background-color:
          {% if state_attr(config.sub_button[0].entity, 'wind_speed') > 20 %}
            var(--warning-color)
          {% elif state_attr(config.sub_button[0].entity, 'wind_speed') > 50 %}
            var(--error-color)
          {% endif %}
          ;
      }
      .bubble-sub-button-2::before {
        content: "km/h";
        margin-left: 2px;
      }
      .bubble-sub-button-3 {
          background-color:
          {% if state_attr(config.sub_button[2].entity, 'forecast')[0].temperature < 10 %}
            var(--info-color)
          {% elif state_attr(config.sub_button[2].entity, 'forecast')[0].temperature > 40 %}
            var(--error-color)
          {% elif state_attr(config.sub_button[2].entity, 'forecast')[0].temperature > 30 %}
            var(--warning-color)
          {% endif %}
          ;
      }
      .bubble-sub-button-3::before {
        content: "°C";
      }
      .bubble-sub-button-4 {
          background-color:
          {% if state_attr(config.sub_button[3].entity, 'forecast')[0].precipitation > 0 %}
            var(--info-color)
          {% endif %}
          ;
      }
      .bubble-sub-button-4::before {
        content:" mm";
        margin-left: 2px;
      }

Weather forecast

weather-forecast.png

While the current weather is important, I also crave a sneak peek at the next week's forecast.

To achieve this, I leveraged the same color-coded system from my weather card for a visually cohesive experience. Additionally, I cleverly formatted the current day using its attributes, while also extracting the high and low temperatures from the same source.

This way, I get a clear picture of the upcoming week's weather trends right on my dashboard.

default:
  - name: ''
  - offset: 1
card:
  type: custom:config-template-card
  variables:
    FORECAST: "states['[[entity]]'].attributes.forecast[[[offset]]].condition"
  entities:
    - "[[entity]]"
  card: 
    type: custom:bubble-card
    card_type: button
    button_type: state
    entity: '[[entity]]'
    name: Forecast
    icon: "${FORECAST === 'partlycloudy' ? 'mdi:weather-partly-cloudy': 'mdi:weather-'+FORECAST}"
    columns: 2
    card_layout: large
    show_state: true
    card_mod:
      style:
        .: |
          .bubble-name {
            color: transparent;
            white-space: nowrap;
          }
          .bubble-name:before {
            color: var(--primary-text-color);
            content: "{{ strptime(state_attr(config.entity, 'forecast')[[[offset]]].datetime, '%Y-%m-%dT%H:%M:%S%z').strftime("%A")  }}";
          }
          .bubble-state {
            color: transparent;
            white-space: nowrap;
          }
          .bubble-state:before {
            color: var(--primary-text-color);
            content: "{{ state_attr(config.entity, 'forecast')[[[offset]]].templow }} / {{ state_attr(config.entity, 'forecast')[[[offset]]].temperature }} {{ state_attr(config.entity, 'temperature_unit') }}";
          }
          .bubble-button-card {
            background-color:
            {% if state_attr(config.entity, 'forecast')[[[offset]]].temperature < 10 %}
              var(--info-color)
            {% elif state_attr(config.entity, 'forecast')[[[offset]]].temperature > 40 %}
              var(--error-color)
            {% elif state_attr(config.entity, 'forecast')[[[offset]]].temperature > 30 %}
              var(--warning-color)
            {% endif %}
            !important;
          }
          .bubble-icon {
            color: 
            {% if state_attr(config.entity, 'forecast')[[[offset]]].temperature < 10 %}
              var(--info-color)
            {% elif state_attr(config.entity, 'forecast')[[[offset]]].temperature > 40 %}
              var(--error-color)
            {% elif state_attr(config.entity, 'forecast')[[[offset]]].temperature > 30 %}
              var(--warning-color)
            {% endif %} !important;
            opacity: 0.6 !important;
          }

Is that it?

While I've highlighted some key customizations, there are additional tweaks throughout my dashboard that refine the presentation further. These refinements build upon the techniques described here. If you're curious to delve deeper, feel free to reach out – I'm always happy to chat about Home Assistant!

For a complete picture, here's a quick reference of the custom cards I've utilized:

  • Alarm Status: bubble_alarm (presented earlier)

  • Air Quality Index: bubble_aqi (presented earlier)

  • Cover Cards: bubble_cover (presented earlier)

  • Section Separators: bubble_separator (presented earlier)

  • Temperature Display: bubble_temperature

  • Thermostat Control: bubble_thermostat (presented earlier)

  • Trash Collection: bubble_trash

  • Vacuum Status: bubble_vacuum

  • Weather Display: bubble_weather (presented earlier)

  • Météo-France Alerts: bubble_weather_alert

  • Future Forecast: bubble_weather_forcecast

  • Website Status: bubble_website

0
Subscribe to my newsletter

Read articles from Bruno Sabot directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bruno Sabot
Bruno Sabot

I am Bruno, a web developer for more than 12 years, mainly working on JavaScript, React, VueJS, HTML, CSS, UX and performance topics. I spend my career mostly on consulting jobs, since I like to share and help other people to grow in their jobs. I'm currently working for (PlayPlay)[https://playplay.com/], a french company. I was also a member of BDX I/O, a French non-profit organization that aims to propose a yearly conference about everything trending in the web technologies. I like to share my discoveries and my expertise, that you will find here, on Hashnode, but also on my Twitter account, where I'm often posting small stories about the tech stuff I like. I'm also a proud father, working on home automation on my spare time, reading about cognitive sciences and I like to cook. I hope you will find all my posts here useful!