A different take on designing a Lovelace UI

it looks like changing the code in that way fixed my problem:

      let swipeCard = this.getRootNode().host,
          gridTitle = swipeCard.getRootNode().querySelector("h1");

really I can not understand why, everything is aligned with @Mattias_Persson dashboard.
Anyone has any idea?

thanks again
Dave

Hi All. Newbie to HA and having some issues with card layouts/grids. Can anyone help me out? I can’t seem to get it to work. Looks fine in ‘vertical view’ but as soon as I change the layout to grid everything messes up. Here is my code:

views:
  - title: Home
    type: custom:grid-layout
    layout_type: grid
    path: 0
    layout:
      display: grid
      grid-template-columns: auto 150px 150px 150px 150px 300px auto
      grid-template-rows: 80px 210px 50px 35px 40px 40px 40px 20px
      grid-gap: 25px
      grid-template-areas: >
        ". clock clock titlemusic weather weather ." 
        ". homecard lightcard personcard1 personcard2 music ."
        ". house lights temperature secure network ."
        ". titledownstairs titledownstairs titleupstairs titleupstairs calendar ." 
        ". livingroom snug hugoroom masterroom ." 
        ". diningroom hallway bathroom ensuiteroom ."
        ". spareroom dressingroom garden garage ." 
        ". footer footer footer footer ."
      mediaquery:
        "(max-width: 1100px)":
          grid-template-columns: auto 150px 150px 150px 150px auto
          grid-template-rows: 80px 210px 50px 35px 40px 40px 40px 250px 20px
          grid-gap: 30px
          grid-template-areas: |
            ". clock clock weather weather ."
            ". homecard lightcard personcard1 personcard2 ."
            ". house lights temperature secure network ."
            ". titledownstairs titledownstairs titleupstairs titleupstairs ."
            ". livingroom snug hugoroom masterroom ."
            ". diningroom hallway bathroom ensuiteroom ."
            ". spareroom dressingroom garden garage ." 
            ". music music calendar calendar ."

welcome @CharlieP1988,

line 4 layout_type: grid and line 7 display: grid of your code are not needed, and should be removed

example from @Mattias_Persson

views:
  - type: custom:grid-layout
    title: Home
    layout:
      #default
      margin: 0
      grid-gap: var(--custom-layout-card-padding)
      grid-template-columns: repeat(4, 1fr) 0
      grid-template-rows: 0 repeat(2, fit-content(100%)) 0fr
      grid-template-areas: |
        "sidebar  .           .       .       ."
        "sidebar  vardagsrum  studio  sovrum  ."
        "sidebar  media       övrigt  hemma   ."
        "sidebar  footer      footer  footer  ."
      mediaquery:
        #phone
        "(max-width: 800px)":
          grid-gap: calc(var(--custom-layout-card-padding) * 1.7)
          grid-template-columns: 0 repeat(2, 1fr) 0
          grid-template-rows: 0 repeat(5, fit-content(100%)) 0fr
          grid-template-areas: |
            ".  .           .        ."
            ".  sidebar     sidebar  ."
            ".  vardagsrum  sovrum   ."
            ".  studio      övrigt   ."
            ".  media       hemma    ."
            ".  footer      footer   ."
            ".  .           .        ."

example from me

views:
  - type: custom:grid-layout
    path: 0
    layout:
      #default
      grid-gap: var(--custom-layout-card-padding)
      grid-template-columns: 0.7fr repeat(3, 1fr) 0.7fr
      grid-template-rows: 0 repeat(2, fit-content(100%)) 0fr
      grid-template-areas: |
        "sidebar  .           .       .       ."
        "sidebar  Living_Room  Rooms  Climate  footer"
        "sidebar  media       Home  People   footer"
        "sidebar   .       .   .  ."
      mediaquery: 
        #phone
        '(max-width: 800px)':
          grid-gap: calc(var(--custom-layout-card-padding) * 1.7)
          grid-template-columns: 0 repeat(2, 1fr) 0
          grid-template-rows: 0 repeat(5, fit-content(100%)) 0fr
          grid-template-areas: |
            ".  .           .        ."
            ".  sidebar     sidebar  ."
            ".  Living_Room  Climate   ."
            ".  Rooms      Home   ."
            ".  media       People    ."
            ".  footer      footer   ."
            ".  .           .        ."

useful links

see this link for more info on layout card: GitHub - thomasloven/lovelace-layout-card: 🔹 Get more control over the placement of lovelace cards.

see this link for help with grid: CSS Grid Layout

2 Likes

Hi @Mattias_Persson and HA fans,

I’m trying to understand this CSS styling with card-mod.
There has been shared multiple different examples and guides, and I’ve managed to successfully use styling with simpler, shorter paths, but this one stumps me:

I’m trying to access this rule in order to remove the border and background from ‘.marker’.

The JS path is:

document.querySelector("body > browser-mod-popup").shadowRoot.querySelector("ha-dialog > div.content > hui-vertical-stack-card").shadowRoot.querySelector("#root > hui-map-card").shadowRoot.querySelector("#root > ha-map").shadowRoot.querySelector("#map > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-marker-pane > div > ha-entity-marker").shadowRoot.querySelector("div")

image

There is already styling on this card, and ‘ha-entity-marker$’ is one of my many failed attempts.

          card_mod:
            style:
              .: |
                #root {
                  height: 25em;
                  padding-bottom: 0 !important;
                }
                ha-icon-button {
                  color: var(--primary-color);
                  zoom: 140%;
                  margin-left: -0.2em;
                }
                ha-card {
                  border-top: 2px solid #1a1a1a;
                  border-radius: 0;
                  transition: none;
                  margin-bottom: -4px !important;
                  height: 25em !important;
                }
              ha-map$: |
                #map {
                  background-color: #191919 !important;
                }
                .leaflet-control-attribution {
                  display: none;
                }
                .leaflet-bar a {
                  background-color: rgba(115, 123, 124, 0.2) !important;
                  color: #9da0a2 !important;
                  backdrop-filter: blur(0.25em);
                  zoom: 140%;
                }
                a.leaflet-control-zoom-in {
                  border-bottom: 1px solid #181818 !important;
                }
                .leaflet-pane.leaflet-tile-pane {
                  filter: invert(0.95) grayscale(0.95) contrast(95%);
                }
              ha-entity-marker$: |
                .marker {
                  border: none !important;
                  background-color: #1c1c1c00 !important;
                }

Please keep in mind that I’ve tried using the card-mod-helper, but I guess I’m not understanding the rules of CSS and/or card-mod.

Hi everyone,

thanks to @Mattias_Persson for creating and sharing this. I am trying to adapt it, but I a stumbling over a few basic problems. They may have been addressed here already, but itś not easy to find anything here :smiley:

Since the german language is famous for its long words I have the problem that most of my entity names do not fit in the cards:

I set show_state: false to make some more room. Then I tried to change the grid-template-areas:

      - grid-template-areas: |
          "icon  circle"
          "n     n"
          "n     n"

But that doesn’t really change much. Can anyone tell me how to span the name over two rows?

10 mins of googling later and I found it Lovelace: Button card - #1719 by RomRider give the post some love if it helps.
for next time this is how I found it “button card home assistant word wrap” in google

first undo all the changes that you have made up to this point that don’t work eg the change to the grid-template-areas.

next in button_card_templates/base.yaml look for

 name:
      - justify-self: start
      - line-height: 121%

replace the above with

    name:
      - justify-self: start
      - line-height: 119% 
      - text-overflow: unset
      - white-space: unset
      - word-break: break-word

feel free to change the line-height to a value that is better for your font size, I think mine is little smaller than the default

1 Like

That worked like a charm. Thank you very much =)
I tried searching for “line-break” in google and the button-card thread, but wasn’t able to find it. Sometimes small details make the difference :wink:

1 Like

i’m trying to make the volume level of a media_player work the same as the brightness of a light, so circle touch to adjust volume.

i tried this but the circle does not react to touch.

            if (domain === 'media_player' && state) {

                // wait 0ms for shadow dom
                setTimeout(() => {

                    // then get elements
                    let elt = this.shadowRoot,
                        circle_slider = elt.getElementById('circle_slider'),
                        circle_value = elt.getElementById('circle_value'),
                        circle_stroke = elt.getElementById('circle_stroke');

                    // approximate position of thumb relative to circle
                    circle_slider.style.top = `${(circle_slider.value - 50) / 1.66 - 1}%`;

                    // debug position
                    let debug = false;
                    if (debug) circle_slider.style.opacity = 0.3;

                    // pass each event to handler
                    ['click', 'input', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach((event) => {
                        circle_slider.addEventListener(event, handler, { passive: true })
                    });

                    function handler(event) {

                        // "this" refers to slider
                        if (event.target === this) {

                            // bypass button-card tap_action
                            event.stopPropagation();

                            // update circle_value
                            circle_value.innerHTML = `${this.value}${tspan}${unit}</tspan>`;

                            // update stroke
                            circle_stroke.style.strokeDashoffset = c - this.value / 100 * c;
                            circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                            
                            // set cursor while dragging
                            if (event.type === 'mousedown' || event.type === 'input') {
                                this.style.cursor = 'grabbing';
                            } else {
                                this.style.cursor = 'grab';
                            }

                            // reset stroke width if value doesn't change
                            if (input == this.value && (event.type === 'click' || event.type === 'touchend'))
                                circle_stroke.style.strokeWidth = 'var(--c-stroke-width)';

                            // on release
                            if (event.type === 'mouseup' || event.type === 'touchend') {

                                // display loader if brightness is 0
                                if (circle_slider.value == 0 && elt.getElementById('loader')) {
                                    elt.getElementById('loader').style.display = 'initial';
                                    elt.getElementById('circle').style.display = 'none';
                                }

                                // set volume
                                hass.callService('media_player', 'volume_set', {
                                    entity_id: entity.entity_id,
                                    volume_level: this.value
                                });
                            }
                        }
                    }
                }, 0);

                return circle(state, input, unit);
            }  
                  ${domain === 'media_player' && `
                      <input id="circle_slider" type="range" min="0" max="1" value="${input}">
                  `}                
                `;

you are spot on with the changes, but you have not updated the css in button_card_templates/extra_styles.yaml

I have set debug to true so you can see the slider in the screenshot

 // debug position
let debug = true;
if (debug) circle_slider.style.opacity = 0.3;

I did this with a little hack as you will see.
as you have not provided the yaml of the button that you are using this on you will have to apply my code to your situation yourself

in extra_styles.yaml look for

        ${this._config.template.includes('light')  ? `

         /* * * * * * * * * * * * * * * * * *
          *                                 *
          *          CIRCLE SLIDER          *
          *                                 *
          * * * * * * * * * * * * * * * * * */

that if condition is only applying the css to buttons that have the light template

I tested this on my sound-bar button and i just used the icon template to apply the styles but it would be better if you had a template that was for your TV not an icon template this was just a quick test

        ${(this._config.template.includes('light') || this._config.template.includes('icon_soundbar'))  ? `

         /* * * * * * * * * * * * * * * * * *
          *                                 *
          *          CIRCLE SLIDER          *
          *                                 *
          * * * * * * * * * * * * * * * * * */

edit
I also did this but I didn’t try what you had so I dont know if that is going to be an issue

                  ${(domain === 'light' || domain === 'media_player') && `
                      <input id="circle_slider" type="range" min="0" max="100" value="${input}">
                  `}

Does anyone know how to have a single button cover 2 columns on 1 row?
I can’t seem to figure it out :sweat_smile:

edit-
Actually, not just 2 columns on 1 row, but rather all columns in 1 row.

cool thanks for the info! i got it working, exept now the custom:swipe-card is prohibiting me from using the slider.

So it works on normal buttons, but when i use a custom: swipe-card to allow for multiple tabs is does not work. Also the lights dont work then.

Do you have any suggestions for that?

EDIT: Strange it does work on the tablet with touch just not on the PC with the mouse

      #################################################
      #                                               #
      #                    leefkeuken                     #
      #                                               #
      #################################################
      - type: grid
        title: Leefkeuken ↔
        view_layout:
          grid-area: leefkeuken
        columns: 1
        cards:

          - type: custom:swipe-card
            parameters:
              speed: 550
              spaceBetween: 40
              threshold: 5
            start_card: 1
            cards:
            
              - type: grid
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: light.keuken
                    name: Keuken Lampen
                    double_tap_action:
                      !include popup/leefkeuken_lampen.yaml
                    template:
                      - light
                      - icon_spot

                  - type: custom:button-card
                    entity: light.eettafel
                    name: Eettafel
                    template:
                      - light
                      - icon_lamp

                  - type: custom:button-card
                    entity: switch.oven_power
                    name: Oven
                    double_tap_action:
                      !include popup/leefkeuken_oven.yaml
                    template:
                      - base
                      - icon_stove
#                      - circle



                  - type: custom:button-card
                    entity: media_player.leefkeuken
                    name: Leefkeuken
                    double_tap_action:
                      !include popup/leefkeuken_monitorer.yaml
                    template:
                      - mediaplayer
                      - icon_monitors


              - type: grid
    #            view_layout:
    #              grid-area: woonkamer
                columns: 2
                cards:

                  - type: custom:button-card
                    entity: climate.leefkeuken
                    name: Verwarming
                    tap_action:
                      !include popup/woonkamer_verwarming.yaml
                    template:
                      - base
                      - icon_climate
                      - circle
                    variables:
                      circle_input: >
                        [[[
                          if (entity) {
                              return entity.state === 'cool'
                                  ? Math.round(entity.attributes.temperature).toString()
                                  : Math.round(entity.attributes.current_temperature).toString();
                          }
                        ]]]
                      circle_input_unit: '°C'

mediaplayer.yaml (template)

mediaplayer:
  template:
    - base
    - circle
    - loader
  double_tap_action:
    action: fire-dom-event
    browser_mod:
      service: browser_mod.popup
      data:
        title: >
          [[[
            return !entity || entity.attributes.friendly_name;
          ]]]
        card_mod:
          style:
            #popup header
            .:
        content:
          type: entities
          card_mod:
            style: |
              #states {
                padding-top: 0.5em;
              }
          entities: >
            [[[
              if (entity) {
                  let lights = [],
                      id = Boolean(entity.attributes.entity_id)
                          ? [entity.entity_id].concat(entity.attributes.entity_id)
                          : [entity.entity_id];

                  for (let i = 0; i < id.length; i++) {
                      lights.push({
                          "type": "custom:mushroom-light-card",
                          "entity": id[i],
                          "fill_container": false,
                          "primary_info": "name",
                          "secondary_info": "state",
                          "icon_type": "icon",
                          "show_brightness_control": true,
                          "use_light_color": true,
                          "show_color_temp_control": true,
                          "show_color_control": true,
                          "collapsible_controls": true
                      });
                  }
                  return lights;
              }
            ]]]
  variables:
    circle_input: >
      [[[
        if (entity) {
            // if light group get brightness from child to remove bounce
            let child = entity.attributes.entity_id,
                volume = child && states[child[0]].attributes.volume_level
                    ? Math.round(states[child[0]].attributes.volume_level * 100)
                    : Math.round(entity.attributes.volume_level * 100);
            return volume === 0 && entity.state !== 'off'
                ? 1
                : volume
        }
      ]]]  
    circle_input_unit: '%'

mediaplayer section in circle.yaml

          /* * * * * * * * * * * * * * * * * *
            *                                 *
            *              MEDIAPLAYER        *
            *                                 *
            * * * * * * * * * * * * * * * * * */

            if (domain === 'media_player' && state) {

                // wait 0ms for shadow dom
                setTimeout(() => {

                    // then get elements
                    let elt = this.shadowRoot,
                        circle_slider = elt.getElementById('circle_slider'),
                        circle_value = elt.getElementById('circle_value'),
                        circle_stroke = elt.getElementById('circle_stroke');

                    // approximate position of thumb relative to circle
                    circle_slider.style.top = `${(circle_slider.value - 50) / 1.66 - 1}%`;

                    // debug position
                    let debug = true;
                    if (debug) circle_slider.style.opacity = 0.3;

                    // pass each event to handler
                    ['click', 'input', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach((event) => {
                        circle_slider.addEventListener(event, handler, { passive: true })
                    });

                    function handler(event) {

                        // "this" refers to slider
                        if (event.target === this) {

                            // bypass button-card tap_action
                            event.stopPropagation();

                            // update circle_value
                            circle_value.innerHTML = `${this.value}${tspan}${unit}</tspan>`;

                            // update stroke
                            circle_stroke.style.strokeDashoffset = c - this.value / 1 * c;
                            circle_stroke.style.strokeWidth = 'var(--c-stroke-width-dragging)';
                            
                            // set cursor while dragging
                            if (event.type === 'mousedown' || event.type === 'input') {
                                this.style.cursor = 'grabbing';
                            } else {
                                this.style.cursor = 'grab';
                            }

                            // reset stroke width if value doesn't change
                            if (input == this.value && (event.type === 'click' || event.type === 'touchend'))
                                circle_stroke.style.strokeWidth = 'var(--c-stroke-width)';

                            // on release
                            if (event.type === 'mouseup' || event.type === 'touchend') {

                                // display loader if brightness is 0
                                if (circle_slider.value == 0 && elt.getElementById('loader')) {
                                    elt.getElementById('loader').style.display = 'initial';
                                    elt.getElementById('circle').style.display = 'none';
                                }

                                // set volume
                                hass.callService('media_player', 'volume_set', {
                                    entity_id: entity.entity_id,
                                    volume_level: this.value / 100
                                });
                            }
                        }
                    }
                }, 0);

                return circle(state, input, unit);
            }  

So the red arrow one is working, the blue arrow ones are showing a hand in the cursor but do not respond to a vertical swipe.

Thanks for sharing this great design. I’m a bit strugling with combining the entities in a row. Can someone direct me in the right direction to get one border around all entities or have no gray border at all.
Schermafbeelding 2023-01-22 205042

- type: grid
        title: Neerslag
        view_layout:
        grid-area: neerslag
        columns: 1
        cards:
          - type: vertical-stack
            cards:
            - type: custom:neerslag-card
              entity: sensor.neerslag_buienalarm_regen_data

            - type: entities
              entities:
              - entity: sensor.tuin_oregon_temperature
                name: Tuin
                icon: mdi:tree

              - entity: sensor.huiskamer_oregon_temperature
                name: Huiskamer
                icon: mdi:sofa

Hello all,
looks like a question asked thousand of times but I’m still not able to make it work.
is there a “clean” way to add a card under the sidebar template aligned to the bottom of the screen?
As of now the only way I’ve found was to add an empty card and fixing its size to add some empty space.

Here’s my code:

  - type: vertical-stack
    view_layout:
      grid-area: sidebar
    cards:
      - type: custom:button-card
        entity: sensor.template_sidebar
        template: sidebar
      - type: 'custom:button-card'
        color_type: blank-card
        card_mod:
          style: |
            ha-card {
              min-height: 25vh !important;
              border-style: none !important
            }

      - type: custom:atomic-calendar-revive
....

This “works” but in case sidebar template adds some more info it is not automatically updated and (even worse) it adds an empty space in my mobile view.

Has anyone found a better solution for that?

Thanks a lot!
Dave

This might not be the “right” way to do it, but you can make it happen with card mod and @media queries to change the look at different screen sizes.

The important part of the first CSS section is position: absolute and bottom: 2vh. 2vh just gives it a slight amount of padding at the bottom rather than the edge of the screen/browser. Also be warned you might have to set or override some other values like width when using position: absolute depending on what card you’re adding and what styles that card has set.

          - type: "custom:atomic-calendar-revive"
            entities:
            - entity: calendar.home_assistant_devs
            card_mod: 
              style: |
                ha-card {
                  border: none;
                  --card-height: inherit !important;
                  position: absolute;
                  bottom: 2vh;
                }
                @media (max-width: 800px) {
                  ha-card {
                    position: inherit;
                  }
                }

1 Like

Yes, it’s possible.
Open template sidebar.yaml and go to section ‘extra_styles’. Modify id ‘#card’ then add min-height value: ‘min-height: 80vh;’ (adapt its value to your screen).


Now go to the section ‘@media screen and (max-width: 800px)’ and add id ‘#card’ with min-height value auto (see image).

That’s all.

This is my result:

1 Like

Hello,

How can I make this popup a bit wider, so that there is no slide bar?
Screenshot 2023-01-23 130441

Thanks!

this might help, A different take on designing a Lovelace UI - #3692 by Mattias_Persson

try this A different take on designing a Lovelace UI - #4072 by Mattias_Persson

thanks @D34DC3N73R and @pajeronda I will have to try your solutions, I am not that happy with my current method for this

1 Like

Thanks @D34DC3N73R I tried it and it partially works.
Card is correctly placed at the bottom of the page but it is no more aligned to the sidebar column (it slightly overlaps other columns).

Any idea?

Thanks!

Ciao @pajeronda I tried also your solution but it works only if I have 2 cards.
On my solution I have 3 cards and changing value on sidebar template moves down the second card (that I want close to the sidebar).

Davide

Ciao @Dave81 if you want to add more cards reduce the min-height value.