is there a way to layout set of vertical stacks straight horizontal? with horizontal scroll?
I donât think so. You can create a row of vertical stacks by putting them inside a horizontal stack, but HA will compress the contents of each card to fit them all in.
There is a HACS frontend feature called Home Assistant Swipe Navigation which allows you to swipe horizontally through views on a dashboard. You could put one or two vertical stacks in each view and scroll through them that way.
This is possible with CSS, you just need to add the style âoverflow-x: scrollâ to the container which contains the cards. Iâm using this for simple sliders, works great and looks like this:
Can you post example code?
Sure, iâm going to post the code for the card in my screenshot as an example. The card is made with âpaper button rowâ and the only important part for the scroll effect is setting âoverflow-x: scrollâ to the parent container. It works best if you give the single cards/buttons within the container a fixed width.
The last card in the code is positioned sticky and is just there for a simple fade-out effect.
type: custom:paper-buttons-row
styles:
justify-content: start
align-items: center
flex-direction: row
gap: 8px
margin: 24px 16px 24px 16px
padding: 0px 0px
overflow-x: scroll
buttons:
- icon: mdi:flash
layout: icon_state
name: Strom
entity: input_boolean.status
ripple: none
tap_action:
action: toggle
confirmation:
text: Alles ausschalten?
state_text:
'on': Strom
'off': aus
state_icons:
'off': mdi:flash-off
state_styles:
'on':
icon:
background-color: var(--accent-color)
styles:
button:
display: flex
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
background-image: |
{% if is_state('input_boolean.status', 'on') %}
linear-gradient(var(--accent-color-background), var(--accent-color-background))
{% else %}
none
{% endif %}
color: var(--text-color)
state:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 26px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:lightbulb-on
layout: icon_name
entity: light.lichtgruppe_alle_lampen
ripple: none
name: Licht
tap_action:
action: navigate
navigation_path: '#popup_licht'
state_icons:
'off': mdi:lightbulb-off-outline
state_styles:
'on':
icon:
background-color: var(--accent-light-on)
button:
display: flex
styles:
button:
display: none
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
background-image: |
{% if is_state('light.lichtgruppe_alle_lampen', 'on') %}
linear-gradient(var(--accent-light-on-background), var(--accent-light-on-background))
{% else %}
none
{% endif %}
color: var(--text-color)
name:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 26px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:thermometer
entity: sensor.temperatur_durchschnitt
layout: icon_state
state:
postfix: °
ripple: none
tap_action:
action: none
styles:
button:
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: rgba(245, 158, 39, 0.1)
color: var(--text-color)
state:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 26px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: rgba(245, 158, 39, 0.25)
- icon: mdi:water
layout: icon_state
state:
postfix: '%'
entity: sensor.luftfeuchtigkeit_durschnitt
ripple: none
tap_action:
action: none
state_icons:
active: mdi:pause-circle
'off': mdi:meditation
styles:
button:
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: rgba(0, 97, 152, 0.15)
color: var(--text-color)
state:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 32px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: rgba(0, 97, 152, 0.25)
- icon: mdi:meditation
layout: icon_name
name: Selfcare
ripple: none
tap_action:
action: navigate
navigation_path: '#popup_selfcare'
entity: timer.selfcare_reminder
state_icons:
active: mdi:pause
'off': mdi:meditation
state_styles:
active:
icon:
background-color: var(--accent-color)
styles:
button:
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
background-image: |
{% if is_state('timer.selfcare_reminder', 'active') %}
linear-gradient(var(--accent-color-background), var(--accent-color-background))
{% else %}
none
{% endif %}
color: var(--text-color)
name:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 32px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:vacuum
layout: icon_name
ripple: none
name: Brudi
tap_action:
action: navigate
navigation_path: '#popup_brudi'
styles:
button:
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
color: var(--text-color)
name:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 24px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:cog-outline
layout: icon_name
name: MenĂź
ripple: none
tap_action:
action: navigate
navigation_path: /config
styles:
button:
padding: 16px 0px
min-width: 94px
max-width: 94px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
color: var(--text-color)
name:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 24px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:pen
layout: icon_name
name: Ăndern
ripple: fill
tap_action:
action: url
url_path: /lovelace/0?disable_km=&edit=1
styles:
button:
margin-right: 0px
padding: 16px 0px
min-width: 90px
max-width: 90px
border-radius: var(--border-radius)
background-color: var(--light-card-background)
color: var(--text-color)
name:
font-weight: 700
font-size: 16px
icon:
'--mdc-icon-size': 24px
color: var(--text-color)
padding: 0px
margin-bottom: 12px
width: 60px
min-width: 60px
max-width: 60px
height: 60px
min-height: 60px
max-height: 60px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: var(--background-color)
- icon: mdi:keyboard-space
layout: icon
ripple: none
tap_action:
action: none
styles:
button:
pointer-events: none
position: sticky
right: '-1px'
margin-left: '-40px'
background-color: transparent
background-image: >-
linear-gradient(90deg,rgba(255,255,255,0) 0%, var(--popup-background)
80%)
color: var(--text-color)
min-width: 50px
height: 118px
border-radius: 0px
z-index: 0
icon:
opacity: 0
Thanks, the paper-button-row makes sense. I tested it on a standard horizontal-stack as well as as a horizontal-stack in a stack-in-card and wasnât successful.
I would get the scroll bar, but the main cards would continue to resize the individual cards.
I do use paper-button-row card so this is helpful!
I am looking to do something like this but vertically with numbers. As a way of controlling my thermostat. Do you think just simply taking your code, stacking the items vertically changing the overflow x to overflow y and restricting the hight of the larent card would give me a vertical scorlling list of numbers?
Yes, this works just fine vertically
Pretty amazing skills here see I. May I ask how you achieved this l we vel? Are you a web designer? Or did you learn this specially?
Yes, I work a lot with css in my job, so it feels easier for me to create such cards with paper button row. I use those horizontal slide rows a lot and improved the code a bit. It now supports features like snap in and conditional fade based on the scroll position:
(Iâm happy to share the yaml if someone is interested)
Would love to see the code for that slider. Honestly the whole dashboard looks AMAZING
Thank you! I am going to share two versions of my horizontal sliders. Both feature a dynamic fade effect on the edges that responds to scroll position (this was hard to achieve with css alone but at some point I was dedicated to solve it haha). The sliders also include a snap-to-place functionality for individual slides.
The first slider is positioned below my dashboard header and includes a conditional card that appears when my oven is active, displaying the remaining cooking time. This approach can be adapted to show conditional cards based on any trigger, with the card being hidden via CSS when inactive. Additionally, Iâve implemented a sticky positioning for the first card, which remains fixed in place and provides quick access to Home Assistant settings:
type: custom:paper-buttons-row
extra_styles: |
div.flex-box:before {
animation-direction: normal;
animation-name: reveal;
animation-duration: 1ms;
animation-timeline: --scroll-timeline;
content: "";
position: absolute !important;
left: 56px;
height: 80px;
width: 80px;
z-index: 1;
pointer-events: none;
background: linear-gradient(to left, transparent 0%,
var(--background-color) 90%);
}
div.flex-box:after {
animation-direction: reverse;
animation-name: reveal;
animation-duration: 1ms;
animation-timeline: --scroll-timeline;
content: "";
position: absolute !important;
pointer-events: none;
right: 16px;
height: 80px;
width: 80px;
pointer-events: none;
background: linear-gradient(to right, transparent 0%,
var(--background-color) 90%);
}
@keyframes reveal {
0% {
opacity: 0;
}
20% {
opacity: 1;
}}
styles:
justify-content: start
gap: 8px
margin: 0px 16px 0px 16px
overflow-x: scroll
scroll-padding-block-start: 200px
overscroll-behavior-x: contain
scroll-snap-type: x mandatory
scroll-timeline: "--scroll-timeline x"
border-radius: var(--border-radius) 0px 0px var(--border-radius)
base_config:
layout: icon
ripple: none
styles:
button:
flex: 1
scroll-snap-align: start
scroll-margin-left: 64px
border-radius: var(--border-radius)
padding: 0px
justify-content: center
background-color: var(--card-background)
icon:
"--mdc-icon-size": 34px
color: var(--text-color)
padding: 0px
min-width: 80px
min-height: 80px
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: transparent
buttons:
- icon: mdi:dots-vertical
tap_action:
action: navigate
navigation_path: /config
styles:
button:
position: sticky
left: 0px
z-index: 2
padding: 0px 4px
margin-right: 8px
min-width: 40px
min-height: 80px
background-color: var(--card-background)
icon:
"--mdc-icon-size": 36px
- icon: mdi:heat-wave
layout: state_name
entity: sensor.mandalofen_verbleibende_zeit
state:
case: lower
name: Min
tap_action:
action: more-info
state_styles:
aus:
button:
opacity: 0
transform: scale(0)
margin-left: "-88px"
styles:
state:
max-width: 60px
pointer-events: none
overflow: hidden
text-overflow: ellipsis
font-weight: 700
font-size: 22px
color: var(--text-color)
name:
max-width: 60px
overflow: hidden
text-overflow: ellipsis
font-weight: 700
font-size: 16px
opacity: 0.5
color: var(--text-color)
button:
display: flex
padding: 0px 0px
min-width: 72px
min-height: 72px
transition: all 0.4s ease-in-out
border-radius: var(--border-radius)
font-size: 16px
border: 4px dashed rgba(207, 44, 12, 0.6)
background-color: rgba(207, 44, 12, 0.1)
- icon: mdi:thermometer
state_icons:
heating: mdi:thermometer
entity: climate.wohnzimmer
state:
attribute: hvac_action
tap_action:
action: navigate
navigation_path: "#raumklima"
styles:
button:
display: flex
icon:
"--mdc-icon-size": 32px
state_styles:
heating:
button:
background-color: var(--thermometer-background)
icon:
color: var(--text-color-active)
- icon: mdi:microphone-message
tap_action:
action: navigate
navigation_path: "#durchsage"
styles:
icon:
"--mdc-icon-size": 34px
- icon: mdi:bathtub-outline
entity: input_boolean.badezeit_toggle
layout: icon_name
name: >
{{ ((today_at(states('input_datetime.badezeit_ende')) -
now()).total_seconds() / 60) |int }}
tap_action:
action: toggle
state_icons:
"on": mdi:bathtub
state_styles:
"on":
button:
background-color: var(--accent-color-dashboard-background)
icon:
display: flex
"--mdc-icon-size": 24px
color: var(--accent-color-dashboard)
margin-bottom: 4px
"off":
name:
display: none
styles:
button:
display: flex
padding: 0px
min-width: 80px
min-height: 80px
transition: all 0.3s ease-in-out
border-radius: var(--border-radius)
background-color: var(--card-background)
color: var(--text-color)
icon:
"--mdc-icon-size": 32px
color: var(--text-color)
padding: 0px
min-height: unset
transition: all 0.3s ease-in-out
border-radius: var(--border-radius)
display: flex
justify-content: center
align-items: center
background-color: transparent
name:
max-width: 30px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
font-weight: 700
font-size: 18px
color: var(--text-color)
- icon: mdi:robot-vacuum
entity: vacuum.brudi
name: Brudi
tap_action:
action: navigate
navigation_path: "#popup_brudi"
state_styles:
cleaning:
button:
background-color: var(--accent-color-dashboard-background)
styles:
icon:
"--mdc-icon-size": 36px
- icon: mdi:pen
entity: input_boolean.show_header
layout: icon
tap_action:
action: toggle
styles:
icon:
"--mdc-icon-size": 32px
The second slider is integrated into a bubble card popup that functions as my TV remote:
This is how it looks when scrolled
type: custom:paper-buttons-row
extra_styles: |
@keyframes reveal-left {
0%, 98% {
opacity: 0;
}
100% {
opacity: 1;
}}
@keyframes reveal-right {
0%, 98% {
opacity: 1;
}
100% {
opacity: 0;
}}
styles:
justify-content: start
gap: 8px
margin: 0px 16px 0px 16px
overflow-x: scroll
overscroll-behavior-x: contain
scroll-snap-type: x mandatory
scroll-timeline: "--scroll-timeline x"
base_config:
entity: media_player.android_tv_wohnzimmer
state:
attribute: source
layout: icon|name
ripple: fill
styles:
icon:
"--mdc-icon-size": 26px
opacity: 0.7
border-radius: 0px
button:
scroll-snap-align: start
padding: 0px 16px
height: 60px
gap: 4px
border-radius: var(--border-radius)
transition: all 0.4s ease-in-out
background-color: var(--card-background)
name:
font-size: 18px
font-weight: 700
white-space: nowrap
color: var(--text-color)
buttons:
- layout: icon
styles:
button:
position: absolute
left: 16px
z-index: 1
height: 60px
border-radius: 0
background: linear-gradient(to left, transparent 0%,var(--background-color) 90%)
animation-name: reveal-left
animation-direction: normal
animation-duration: 300ms
animation-timeline: "--scroll-timeline"
pointer-events: none
icon:
opacity: 0
- name: TV
icon: mdi:television-shimmer
image: /local/Icons/apps/waipu-tv.png
state_styles:
de.exaring.waipu:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: script.tv_wohnzimmer_fernsehen
- name: Netflix
icon: mdi:netflix
state_styles:
netflix:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: media_player.select_source
target:
entity_id: media_player.android_tv_wohnzimmer
service_data:
source: Netflix
styles:
icon:
"--mdc-icon-size": 22px
color: "#E50914"
- name: Prime
icon: mdi:television
image: /local/Icons/apps/prime.svg
state_styles:
prime video:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: media_player.select_source
target:
entity_id: media_player.android_tv_wohnzimmer
service_data:
source: com.amazon.amazonvideo.livingroom
styles:
icon:
"--mdc-icon-size": 26px
width: 20px
height: 20px
opacity: 1
- name: Disney
icon: mdi:television
image: /local/Icons/apps/disney_plus.svg
state_styles:
disney+:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: media_player.select_source
target:
entity_id: media_player.android_tv_wohnzimmer
service_data:
source: com.disney.disneyplus
- name: WOW
icon: mdi:television
image: /local/Icons/apps/wow.svg
state_styles:
de.sky.online:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: media_player.select_source
target:
entity_id: media_player.android_tv_wohnzimmer
service_data:
source: de.sky.online
- name: YouTube
icon: mdi:youtube
state_styles:
com.liskovsoft.smarttubetv.beta:
button:
background-color: var(--accent-color-dashboard-background)
tap_action:
action: call-service
service: media_player.select_source
target:
entity_id: media_player.android_tv_wohnzimmer
service_data:
source: com.liskovsoft.smarttubetv.beta
styles:
icon:
"--mdc-icon-size": 26px
color: "#CD201F"
- layout: icon
styles:
button:
position: absolute
right: 16px
z-index: 1
height: 60px
border-radius: 0
background: linear-gradient(to right, transparent 0%,var(--background-color) 90%)
animation-direction: normal
animation-name: reveal-right
animation-duration: 300ms
animation-timeline: "--scroll-timeline"
pointer-events: none
icon:
opacity: 0
The code for both sliders includes various customizations which have to be edited out/replaced with your own, such as css variables, touch actions and conditional styles. Iâve left these in the code to serve as examples (and, admittedly, because Iâm too lazy to edit it out).
Would you able to share your full dashboard? Looks amazing!
Thanks Iâve thought about sharing my dashboard but there are so many easier/better alternatives (like for example bubble cards) than doing it the way I did. Because I was so annoyed by the constant customization with card mod, I have implemented everything almost exclusively with those super customized paper button rows. I honestly donât know if that would be useful for others. I think Iâll test it with a post for a single element like the TV card
Hello,
Is it possible to hide that scroll bar/thumb appearing when scrolling? I have not found any info how to do it. Tried changing themes but did not help.
Do you guys also have it or managed to hide it somehow?
Please see the video Imgur: The magic of the Internet
Nice idea. Adding âscrollbar-width: noneâ to the parent container hides the scrollbar in chrome.
The code is probably slightly different for Mozilla/webkit, so you have to add all of its variations if you want to cover all browsers
This is what I was looking for. Thanks!