Installation instructions further down
Just thought I would share what I’ve been working on. I’ve spent a while trying to make traditional lovelace cards look good but the variance in sizes and designs left me frustrated with the final result. I realised that a tile-based UI would go a long way to making things look cohesive - similar sized tesselating tiles - and took a great deal of inspiration from existing Homekit-style designs. However, Home Assistant isn’t Homekit, and I didn’t want some pseudo-imitation - I want my own thing!
This is the end result:
As with any installation you can add and add to it but about 90% of this UI can be created from a fresh HA setup using just two custom cards:
-
The simply fantastic Button Card by @RomRider - every card (excluding pop-ups) and text element in this UI is made from these button cards. I have made templates for entities, media players, alarm panels, weather UI, sensors, persons, text headers, and temperature cards, all of which can be quickly replicated.
-
The excellent Layout Card by @thomasloven - all cards slot into a user-definable grid configuration making quick rearrangements of the layout very straightforward - no more mucking about with nesting millions of vertical-and-horizontal-stack cards!
-
…that’s pretty much it!
Things I used to make it prettier:
-
The Google Dark and Google Light themes by @JuanM with some tiny adjustments for personal preference
-
@kalkih’s great Mini Graph card and Mini Media Player for popup graphs and granular media controls
-
Auto Entities (again by @thomasloven) for the controls behind the Active Entities text on the Home page
Thank you to everyone mentioned for your contributions to the Home Assistant Community!
Installation Instructions
You’ll need the two required cards (Button Card and Layout Card), either manually or preferentially using HACS by @ludeeus.
You’ll first need to set up your view as a grid:
title: My House
views:
- path: home
title: Home
theme: Backend-selected
badges: []
panel: true
cards:
- type: 'custom:layout-card'
column_width: 100%
layout: vertical
cards:
- type: 'custom:layout-card'
layout: grid
gridcols: 158px 158px 158px 158px 158px 158px # more on this below
gridrows: 158px 158px 158px 158px 158px 158px # more on this below
cards:
Your column and row sizes need to be based on your desired layout and the sizes of the cards you’re using. The card templates I have made are mostly 150px tall by 150px wide and I’ve been using an 8px gap between cards, so by default I set my rows and columns to 158px in size to allow for the card and the gap. You can of course set a wider gap if you like!
The double-sized cards are therefore 150px tall by 308px wide in order to line up with the other cards (150px + 150px + 8px gap). You don’t need to make the column 316px wide because the wide card will simply overlap 2 grid spaces - just make sure if your wide card is in column 1 that you don’t put anything in column 2 next to it because they will overlap!
To use the card templates, add the following to the start of either lovelace.yaml or the built-in configuration editor (above where you define your views):
button_card_templates:
Then add any of the following:
Alarm card:
308px wide
button_card_templates:
alarm:
show_state: true
size: 20%
state:
- icon: 'mdi:shield-check'
styles:
icon:
- color: var(--label-badge-green)
value: disarmed
- icon: 'mdi:shield-lock'
styles:
icon:
- color: var(--label-badge-red)
value: armed_away
styles:
card:
- width: 308px
- height: 150px
grid:
- grid-template-areas: '"i" "n" "s"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- align-content: start
- justify-content: start
- margin-left: 20px
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- margin-bottom: '-6px'
- font-weight: lighter
tap_action:
action: call-service
service: script.alarm_toggle
# You'll need to make a quick script to toggle your alarm on/off -
# I tried doing this with templates in-card but it caused issues
# with the state not updating correctly
Default (for lights, switches, etc):
default:
tap_action:
action: toggle
hold_action:
action: more-info
show_state: true
size: 30%
state:
- styles:
card:
- filter: opacity(50%)
icon:
- filter: grayscale(100%)
value: 'off'
- styles:
card:
- filter: opacity(25%)
icon:
- filter: grayscale(100%)
value: unavailable
styles:
card:
- width: 150px
- height: 150px
grid:
- grid-template-areas: '"i" "n" "s"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- justify-content: start
- margin-left: 20px
- margin-bottom: 30px
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
Garbage (requires Garbage Collection sensor by @bruxy70):
garbage:
show_label: true
size: 30%
state:
- label: '[[[ return `in ${entity.attributes.days} days` ]]]'
value: 2
- label: Tomorrow
value: 1
- label: Today
value: 0
styles:
card:
- width: 150px
- height: 150px
grid:
- grid-template-areas: '"i" "n" "l"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- justify-content: start
- margin-left: 20px
- margin-bottom: 30px
label:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
name:
- justify-self: start
- margin-left: 10px
Header:
Thin header text for above a group of cards. 40px tall so needs a row height of 40px if more text below it or 60px if directly above a set of cards:
header:
show_icon: false
show_name: true
show_state: false
styles:
card:
- width: 300px
- height: 40px
- background: none
- box-shadow: none
name:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
- font-size: x-large
Media (my favourite card!):
308px wide
media:
tap_action:
action: call-service
service: media_player.toggle
service_data:
entity_id: entity
show_state: true
show_label: true
state:
- value: idle
label: ' '
- styles:
card:
- filter: opacity(50%)
icon:
- filter: grayscale(100%)
label: ' '
value: 'off'
- styles:
card:
- filter: opacity(25%)
icon:
- filter: grayscale(100%)
label: ' '
value: unavailable
label: >-
[[[ if (entity.attributes.media_title == undefined) return " ";
else if (entity.attributes.media_artist == undefined) return
`${entity.attributes.media_title}`; else return
`${entity.attributes.media_title} - ${entity.attributes.media_artist}` ]]]
size: 12%
styles:
card:
- width: 308px
- height: 150px
- background-size: cover
- background-image: '[[[ return `url("${entity.attributes.entity_picture}")` ]]]'
- background-position: center center
grid:
- grid-template-areas: '"i i" "n n" "s l"'
- grid-template-columns: 1fr 2fr
- grid-template-rows: 1fr min-content min-content
- background-image: >
[[[ return `linear-gradient(to top,
var(--paper-card-background-color),
var(--paper-card-background-color-transparent))` ]]]
- margin-bottom: '-25px'
img_cell:
- align-content: start
- justify-content: start
- margin-left: 0px
- margin-bottom: 50px
icon:
- filter: drop-shadow(0px 0px 10px var(--paper-card-background-color))
- padding-left: 15px
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- margin-bottom: 6px
- font-weight: lighter
label:
- justify-self: start
- margin-left: '-20px'
- margin-bottom: 6px
- font-weight: lighter
Person:
person:
show_entity_picture: true
show_state: true
state:
- operator: '!='
styles:
card:
- filter: opacity(50%)
icon:
- filter: grayscale(100%)
value: home
styles:
card:
- width: 150px
- height: 150px
grid:
- grid-template-areas: '"i" "n" "s"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
icon:
- border-radius: 50%
img_cell:
- justify-content: start
- margin-left: 20px
- margin-bottom: 30px
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
Sensor:
sensor:
show_state: true
size: 30%
styles:
card:
- width: 150px
- height: 150px
grid:
- grid-template-areas: '"i" "n" "s"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- justify-content: start
- margin-left: 20px
- margin-bottom: 30px
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
Temperature:
Shows a large temperature reading in-card
temperature:
label: Temperature
show_label: true
show_state: true
size: 80%
styles:
card:
- width: 150px
- height: 150px
grid:
- grid-template-areas: '"i s" "n n" "l l"'
- grid-template-columns: 1fr 2fr
- grid-template-rows: 1fr min-content
img_cell:
- margin-bottom: 30px
label:
- justify-self: start
- margin-left: 10px
- font-weight: lighter
name:
- justify-self: start
- margin-left: 10px
state:
- margin-bottom: 30px
- margin-right: 40px
- font-size: x-large
Weather:
308px wide. I’m using DarkSky as my source… at least until Apple shuts it down…
weather:
label: '[[[ return `${entity.attributes.temperature}°C` ]]]'
show_label: true
show_state: true
size: 70%
state:
- icon: 'mdi:weather-night'
value: clear-night
- icon: 'mdi:weather-cloudy'
value: cloudy
- icon: 'mdi:weather-fog'
value: fog
- icon: 'mdi:weather-hail'
value: hail
- icon: 'mdi:weather-lightning'
value: lightning
- icon: 'mdi:weather-lightning-rainy'
value: lightning-rainy
- icon: 'mdi:weather-partly-cloudy'
value: partlycloudy
- icon: 'mdi:weather-pouring'
value: pouring
- icon: 'mdi:weather-rainy'
value: rainy
- icon: 'mdi:weather-snowy'
value: snowy
- icon: 'mdi:weather-snowy-rainy'
value: snowy-rainy
- icon: 'mdi:weather-sunny'
value: sunny
- icon: 'mdi:weather-windy'
value: windy
- icon: 'mdi:weather-windy-variant'
value: windy-variant
- icon: 'mdi:weather-cloudy-alert'
value: exceptional
styles:
card:
- width: 308px
- height: 150px
grid:
- grid-template-areas: '"i l" "n n" "s s"'
- grid-template-columns: 1fr 2fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- align-content: start
- justify-content: start
- margin-left: 20px
label:
- justify-self: start
- font-size: 250%
name:
- justify-self: start
- margin-left: 10px
state:
- justify-self: start
- margin-left: 10px
- margin-bottom: '-6px'
- font-weight: lighter
Good morning/afternoon/evening header: See here.
Active entities headers: See here.
Putting it all together
To add your first card to the view, simply add a button card, assign it an entity and one of the templates above, and at it to a column and row. For example:
title: My House
views:
- path: home #
title: Home #
theme: Backend-selected # Your view setup
badges: [] #
panel: true #
cards:
- type: 'custom:layout-card' #
column_width: 100% # For setting up panel view
layout: vertical #
cards:
- type: 'custom:layout-card' #
layout: grid # Grid
gridcols: 158px 158px 158px 158px 158px 158px # setup
gridrows: 158px 158px 158px 158px 158px 158px #
cards:
- type: 'custom:button-card' #
entity: light.living_room_tv_lamp #
gridcol: 1/6 # Your first
gridrow: 1/6 # card
name: TV Lamp #
template: default #
…will produce this:
Rinse, lather, repeat to create your own modular, clean, tile-based UI!
Known issues
Wide (308px) tiles positioned at the end of a row (e.g. gridcol 5/6) will display slightly incorrect margins for name and state text. The workaround for this is simply to add an extra column to the end of your grid!