Crafting My Perfect Home Assistant Dashboard
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
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
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
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
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
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
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
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
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
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!