Fun with custom:button-card

There is one “problem” with the active stroke, it is already full around 85%
I think that is because the start of the active border is not in the middle (but on the left) of the rect?

image

custom_fields:
  RectP: |
    [[[ 
      const state = states['sensor.afzuigkap_programma_voortgang'].state;
      let bgActiveColor;
        if (state >= 90) {
          bgActiveColor = '#00ae5b';
        } else if (state >= 50) {
          bgActiveColor = '#ffbe3e';
        } else if (state >= 25) {
          bgActiveColor = '#ff9532';
        } else if (state >= 0) {
          bgActiveColor = '#ff3e3e';
        } else {
          bgActiveColor = 'rgba(217,217,217,0.1)';
        }
      const value = Math.abs(states['sensor.afzuigkap_programma_voortgang'].state);
      const fill = 'rgba(255,255,255,0.7)';
      const bgColor = '#d9d9d9';
      const width = 60;
      const height = 26;
      const dashArray = 2 * (width + height);
      const dashOffset = dashArray * (1 - value / 100); 

      return `
      <svg viewBox="0 0 75 75">
        <rect x="8" y="8" width="${width}" height="${height}"
          rx="12" ry="12"
          stroke="${bgColor}" stroke-width="1" fill="${fill}" />
        <rect x="8" y="8" width="${width}" height="${height}"
          rx="12" ry="12"
          stroke="${bgActiveColor}" stroke-width="2" fill="none"
          stroke-dasharray="${dashArray}" stroke-dashoffset="${dashOffset}" />
        <text x="38" y="23" fill="rgba(62, 62, 62, 1.0);" 
          font-weight="normal" font-size="16" font-family="Sans-serif" 
          text-anchor="middle" dominant-baseline="middle" alignment-baseline="middle">
          ${value}%
        </text>
      </svg>`;
    ]]]

no, not at all. you can use that same entity in the javascript .

seems to me you are way over your head here, creating difficult configs, while not understanding things.
simply c&p stuff we make for you wont help you in the long run

please educate yourself step by step, and make progress doing so.

playing with entities across the board in the various configurations is basic and required knowledge you should grasp first.

1 Like

Because you manipulate the border radius, the total stroke length is cut off…

const dashArray = 2 * ((width - 6) + (height - 5))
1 Like

:man_facepalming: yes of course haha
Thanks again! have it all working now :smiley:

care to have a look at New badges and card-mod customisation - #67 by Mariusthvdb please?

Seeking the border percentage on a badge is just a but more involved…

I did encounter a problem that if the entity used in the custom field is removed and no longer exists the whole card will not load anymore.

I tried the following but no success:

custom_fields:
  TemperatureC: |
    [[[
      const state = states['sensor.panasonic_wandmodel_woonkamer_inside_temperature'].state;
      if (state is defined) {
      return `
        <ha-card style="width: 55px; height: 25px; padding: 0px 2px 0px 2px;
          color: rgba(62, 62, 62, 1.0); background-color: rgba(255,255,255,0.8); border-color: #d9d9d9; border-radius: 12px;
          font-size: 14px; text-transform: capitalize; font-family: Sans-serif;">
          <span>${states['sensor.panasonic_wandmodel_woonkamer_inside_temperature'].state} °C</span>
        </ha-card> `
      }
    ]]]

Is there any other way to check if the entity exists?

I wanted to share the swipable (over the next 8hrs) weather template that i’ve been trying to refine and optimize for some time now. I use it as a weather preview for the coming hours taking into account if its day or nighttime including current conditions at the top.
The times are full hours past the current time and the weather shows only the temps and conditions.
Now the only thing that’s missing is to inject the time when the sun rises and sets with hours AND minutes. Haven’t figured that out yet.
Any suggestions?

here’s a code snippet from my template:

        - type: custom:button-card
          entity: '[[[ return entity.entity_id ]]]'
          label: >-
            [[[ 
              var index = 6;
              var temperature = parseFloat(states['sensor.weather_temp_h' + index].state);
              return Math.round(temperature) + "<span style>°C</span>"; 
            ]]]
          name: >
            [[[ 
              var index = 6;
              var now = new Date();
              var currentHour = now.getHours();
              var hourOffset = index + 1;
              var nextHour = (currentHour + hourOffset) % 24;
              var period = nextHour >= 12 ? "PM" : "AM";
              var formattedHour = nextHour % 12 === 0 ? 12 : nextHour % 12;
              return formattedHour + '<span style="font-size: 0.8rem;">' + period + '</span>';
            ]]]
          show_label: true
          show_icon: false
          custom_fields:
            icon: >
              [[[ 
                var index = 6;
                var weather = states['sensor.weather_condition_h' + index].state;
                
                var forecastTime = new Date();
                forecastTime.setHours(forecastTime.getHours() + (index + 1));

                var sunriseTime = new Date(states['sensor.home_sun_dawn'].state);
                var sunsetTime = new Date(states['sensor.home_sun_dusk'].state);

                sunriseTime.setFullYear(forecastTime.getFullYear());
                sunriseTime.setMonth(forecastTime.getMonth());
                sunriseTime.setDate(forecastTime.getDate());

                sunsetTime.setFullYear(forecastTime.getFullYear());
                sunsetTime.setMonth(forecastTime.getMonth());
                sunsetTime.setDate(forecastTime.getDate());

                var isNight = forecastTime < sunriseTime || forecastTime >= sunsetTime;

                var iconSuffix = (isNight && !weather.startsWith('clear')) ? '-night' : '';

                return '<img src="/local/weather-icons/' + weather + iconSuffix + '.svg"/>';
              ]]]
          styles: *item_styles


So this is my weather dashboard using the CB-lacars buttons CB=Custom Button
it is animated you can see that here

1 Like

I need your help :pray: I created and modified a beautiful sticky navigation bar based on this (even my wife loves it :grin:). While trying to further evolve the bar, I came across an issue with the custom:button-card:

Is there a way to trigger the refresh of the button based on the return of a javascript? Goal: I would like to highlight the background of the button on the navigation bar based on the currently open view. However, the button does not refresh on a change and “triggers_update: all” only updates the button with an ugly delay.

Currently, the button configuration looks as follows:

            - type: custom:button-card
              icon: mdi:cog
              tap_action:
                action: navigate
                navigation_path: |
                  [[[
                    let popup_name = '#pop-up-system';
                    let current_path = window.location.href;
                    let popup_path_position = current_path.lastIndexOf(popup_name);
                    if (popup_path_position != -1) {
                      popup_name = current_path.substring(0, popup_path_position);
                    };
                    return popup_name;
                  ]]]     
              triggers_update: all
              styles:
                icon:
                  - width: 24px
                  - color: white
                img_cell:
                  - width: 50px
                  - height: 50px
                card:
                  - background: |
                      [[[
                        let popup = window.location.hash;
                        if (popup == '#pop-up-system') return 'grey';
                        else return 'none';
                      ]]]
                  - border: none
                  - padding: 0

You’ll have to use a helper such as an input select or input boolean

Hmm, but that would affect the navigation bar on all client devices at the same time, correct? Is there a way to have client-specific helpers?

What comes to mind is to make an automation that sets the helper based on the user.

Im working on my floorplan dashboard at the moment.

For additional person cards i want to use custom:button-card in a hexagon style.
Grid_Button-Card_Person
I want to set the round entity picture in the middle of the upper half.
On the lower half i want to set the name and the state of my person entity.
But as u see it didnt get to work.

Heres my code:

          - square: true
            type: grid
            card_mod:
              style: |
                :host {
                  position: absolute;
                  left: 21%;
                  top: 7%;
                  opacity: 0.7;
                  mix-blend-mode: normal;
                  }
            cards:
              - type: custom:button-card
                card_mod:
                  style: |
                    :host {
                      aspect-ratio: 1/cos(30deg);
                      clip-path: polygon(50% -50%,100% 50%,50% 150%,0 50%);
                      background: #3B8686;
                    }
                entity: person.micha
                name: Micha
                aspect_ratio: 2/1
                show_entity_picture: false
                state:
                  - value: home
                    styles:
                      custom_fields:
                        icon:
                          - border-color: '#77c66e'
                  - value: not_home
                    styles:
                      custom_fields:
                        icon:
                          - border-color: deepskyblue
                  - value: Dossmann
                    styles:
                      custom_fields:
                        icon:
                          - border-color: '#B83838'
                styles:
                  grid:
                    - grid-template-areas: >-
                        "icon"  
                        "name"
                        "state"
                    - grid-template-columns: 1fr
                    - grid-template-rows: 1fr 
                  custom_fields:
                    icon:
                      - clip-path: circle()
                      - width: 50%
                      - pointer-events: none
                      - display: grid
                      - border: 3px solid
                      - border-color: '#808080'
                      - border-radius: 500px
                      - margin: 0 0 0 0
                      - opacity: 1
                      - align-self: start
                      - justify-self: end
                    name:
                      - align-self: end
                      - justify-self: end
                      - background: none
                      - padding: 5px
                    state:
                      - align-self: start
                      - justify-self: middle
                      - background: none
                      - padding: 5px
                      - margin-top: 5px
                custom_fields:
                  icon: >
                    [[[ return entity === undefined ? null : `<img
                    src="${states[entity.entity_id].attributes.entity_picture}"
                    width="100%">`; ]]]
                  name:
                    card:
                      type: custom:button-card
                      name: Micha
                      styles:
                        card:
                          - color: var(--contrast20)
                          - font-size: 14px
                          - font-weight: 600
                          - background: none
                  state:
                    card:
                      type: custom:button-card
                      entity: person.micha
                      show_icon: false
                      name: |
                        [[[ 
                          return states['person.micha'].state;
                        ]]]
                      state:
                        - value: home
                          name: Daheim
                        - value: not_home
                          name: Unterwegs
                      styles:
                        card:
                          - color: var(--contrast20)
                          - font-size: 13px
                          - background: none
                          - opacity: '0.7'
              - type: custom:button-card
                entity: person.isa
                show_state: true
                show_entity_picture: false
                card_mod:
                  style: |
                    :host {
                      top: 45%;
                      left: -32%;
                      aspect-ratio: 1/cos(30deg);
                      clip-path: polygon(50% -50%,100% 50%,50% 150%,0 50%);
                      background: #3B8686;
                      border: #434589;
                    }
              - type: custom:button-card
                entity: input_boolean.hund_daheim
                name: Willow
                show_entity_picture: false
                styles:
                  entity_picture:
                    - size: 20%
                entity_picture: /local/willow.jpg
                card_mod:
                  style: |
                    :host {
                      left: -63%;
                      aspect-ratio: 1/cos(30deg);
                      clip-path: polygon(50% -50%,100% 50%,50% 150%,0 50%);
                      background: #3B8686;
                    }
            columns: 3

I only use a grid, because i didnt know how i can set a absolute position for a custom-button card. :sweat_smile:

Maybe someone could help me? :slight_smile:

This code renders correctly:

type: custom:button-card
entity: person.tom
show_state: true
show_name: false
show_label: true
label: "[[[ return helpers.relativeTime(states['device_tracker.tom_phone'].last_changed); ]]]"

Now I would like it to instead say “Last Updated: X time ago”. But this seems to fail, rendering as [object Object]:

label: "[[[ return 'Last Updated: ' + helpers.relativeTime(states['device_tracker.tom_phone'].last_changed); ]]]"

Trying as well to copy the example from the documentation here also gives the same result:

label: >
  [[[
    var lastupdated = helpers.relativeTime(states['device_tracker.tom_phone'].last_changed);
    return 'Last Updated: ' + (lastupdated);
  ]]]

Can anyone help with how to achieve this?

This is apparently a known issue, and a workaround is shown here:

I’m trying to pull some latency values from sensors in The Unifi integration into a custom-button-card. The value, in milliseconds, will show up correct in a stadard entities card, however in the custom-card it shows as seconds. See screenshots and code bellow.
How do I format or template the value to show up correct, in ms in the custom-button-card?

From the custom-button-card:

From the stock entity card:

My code:

entity: sensor.hytta_hh_cloudflare_wan_latency
name: Latency Starlink
show_icon: true
show_name: true
show_state: true
styles:
  card:
    - background: var(--card-background-color)
    - height: 70px
    - border-radius: 20px
    - padding: 0px
  name:
    - position: absolute
    - bottom: 45%
    - left: 40%
    - font-size: 15px
    - font-weight: 600
    - color: var(--primary-color)
  state:
    - position: absolute
    - bottom: 20%
    - left: 40%
    - font-size: 15px
    - font-weight: 550
    - color: rgba(96,114,116,0.7)
  icon:
    - position: absolute
    - bottom: 11%
    - left: 2%
    - height: 55px
    - color: var(--bedroom-blue)
type: custom:button-card
entity: sensor.hytta_hh_cloudflare_wan_latency
name: Latency Starlink
show_icon: true
show_name: true
show_state: true
styles:
  card:
    - background: var(--card-background-color)
    - height: 70px
    - border-radius: 20px
    - padding: 0px
  name:
    - position: absolute
    - bottom: 45%
    - left: 40%
    - font-size: 15px
    - font-weight: 600
    - color: var(--primary-color)
  state:
    - position: absolute
    - bottom: 20%
    - left: 40%
    - font-size: 15px
    - font-weight: 550
    - color: rgba(96,114,116,0.7)
  icon:
    - position: absolute
    - bottom: 11%
    - left: 2%
    - height: 55px
    - color: var(--bedroom-blue)
type: custom:button-card
entity: sensor.hytta_hh_cloudflare_wan_latency
name: Latency Starlink
show_icon: true
show_name: true
show_state: true
styles:
  card:
    - background: var(--card-background-color)
    - height: 70px
    - border-radius: 20px
    - padding: 0px
  name:
    - position: absolute
    - bottom: 45%
    - left: 40%
    - font-size: 15px
    - font-weight: 600
    - color: var(--primary-color)
  state:
    - position: absolute
    - bottom: 20%
    - left: 40%
    - font-size: 15px
    - font-weight: 550
    - color: rgba(96,114,116,0.7)
  icon:
    - position: absolute
    - bottom: 11%
    - left: 2%
    - height: 55px
    - color: var(--bedroom-blue)

I’m trying to get a slider-entity-row card to work inside of a button-card. But I’m messing the grid up. I got it working right on desktop but the formatting is all messed up on mobile. See below:

Screenshot 2025-02-07 094059

Can anyone assist in correcting the code so the mobile and desktop formatting are identical?

type: custom:button-card
name: Living Room
entity: light.living_room_lights
icon: mdi:light-recessed
tap_action:
  action: toggle
hold_action: []
show_icon: true
show_state: true
styles:
  card:
    - height: 60px
    - border-radius: 15px
    - font-size: 10px
    - padding-bottom: 0px
    - padding-top: 0px
    - position: relative
  img_cell:
    - align-self: center
    - justify-content: center
  grid:
    - grid-template-areas: "\"i n n\" \" i slider s\""
    - grid-template-columns: 50px auto
    - grid-template-rows: auto auto
  name:
    - justify-self: left
    - font-weight: 600
    - font-size: 14px
    - padding-top: 20px
    - padding-left: 6px
    - padding-bottom: 0px
  state:
    - justify-self: left
    - font-size: 12px
    - font-weight: 400
    - padding-bottom: 30px
    - padding-left: 0px
  icon:
    - justify-self: center
    - padding-top: 0px
    - padding-left: 10px
    - padding-bottom: 30px
    - width: 25px
  custom_fields:
    slider:
      - width: 90%
      - margin-left: 0%
      - justify-self: left
      - font-size: 16px
      - padding-bottom: 30px
      - padding-top: 0px
      - padding-left: 0px
state:
  - value: "off"
    styles:
      card: null
      icon: null
  - value: "on"
    styles:
      card:
        - background-color: rgba(255, 255, 255,.8)
      icon:
        - color: "#FF9500"
      name:
        - color: black
      state:
        - color: black
      custom_fields:
        slider:
          - color: black
custom_fields:
  slider:
    card:
      full_row: true
      type: custom:slider-entity-row
      entity: "[[[ return entity.entity_id ]]]"
      hide_state: true

I am hoping someone can help me because I am tired of fighting with AI to try to figure this out. I am using custom:button-card for my entire dashboard. I am using a base template as shown below.

base:
  variables:
    state_on: >
      [[[ return ['on', 'home', 'heat_cool', 'Running', 'Fresh', 'Charging', 'charging', 'Started', 'locked', 'Empty', 'Dirty', 'Auto'].indexOf(!entity || entity.state) !== -1; ]]]
    state_home: >
      [[[ return ['home'].indexOf(!entity || entity.state) !== -1; ]]]
    state_zone: >
      [[[ return ['Flinner', 'Kelly', 'Stantec', 'Friendship', 'TPA', 'SVS'].indexOf(!entity || entity.state) !== -1; ]]]
    state_away: >
      [[[ return ['not_home'].indexOf(!entity || entity.state) !== -1; ]]]
    state_empty: >
      [[[ return ['Empty'].indexOf(!entity || entity.state) !== -1; ]]]
    state_done: >
      [[[ return ['Done'].indexOf(!entity || entity.state) !== -1; ]]]
    state_auto: >
      [[[ return ['Auto'].indexOf(!entity || entity.state) !== -1; ]]]
    state_used: >
      [[[ return ['Used'].indexOf(!entity || entity.state) !== -1; ]]]
    state_clean: >
      [[[ return ['Clean'].indexOf(!entity || entity.state) !== -1; ]]]
    state_dirty: >
      [[[ return ['Dirty'].indexOf(!entity || entity.state) !== -1; ]]]
    state_charging: >
      [[[ return ['Charging', 'charging'].indexOf(!entity || entity.state) !== -1; ]]]
    state_discharging: >
      [[[ return ['Not Charging', 'not charging', 'discharging'].indexOf(!entity || entity.state) !== -1; ]]]
    state_unlocked: >
      [[[ return ['unlocked'].indexOf(!entity || entity.state) !== -1; ]]]
    state_locked: >
      [[[ return ['locked'].indexOf(!entity || entity.state) !== -1; ]]]
    state: >
      [[[ return !entity || entity.state; ]]]
    entity_id: >
      [[[ return !entity || entity.entity_id; ]]]
    entity_picture: >
      [[[ return !entity || entity.attributes.entity_picture; ]]]
    is_youtube: >
      [[[
        let is_youtube = entity?.attributes?.app_id === 'com.google.ios.youtube',
            sensor = this?._config?.triggers_update,
            media_title = entity?.attributes?.media_title,
            watching_title = states[sensor]?.attributes?.title;
        if (is_youtube && media_title === watching_title) {
            return true;
        }
      ]]]
        translate_home: Home
    translate_not_home: Away
    translate_open: Open
    translate_closed: Closed
    translate_occupied: Occupied
    translate_not_occupied: Empty
    translate_on: "On"
    translate_off: "Off"
    translate_detected: "Detected"
    translate_clear: "Clear"
    translate_playing: "Playing"
    translate_paused: "Paused"
    translate_NotCharging: "Discharging"
  triggers_update:
    - "[[[ return variables.sensor_entity; ]]]"
    - "[[[ return variables.timer_entity; ]]]"
  tap_action:
    action: more-info
  aspect_ratio: 1/1
  show_state: true
  show_icon: false
  styles:
    grid:
      - grid-template-areas: |
          "icon   circle"
          "n      n"
          "s      s"
      - grid-template-columns: repeat(2, 1fr)
      - grid-template-rows: auto repeat(3, min-content)
      - gap: 1.3%
      - align-items: start
      - will-change: transform
      - position: relative
    name:
      - justify-self: start
      - line-height: 110%
      - font-size: 15px
      - font-weight: bold
    state:
      - justify-self: start
      - line-height: 110%
      - font-size: 12px
    card:
      - background-image: >
          [[[
            if (variables.state_home) {
              return `linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)`;
            }
            if (variables.state_away) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_zone) {
              return `linear-gradient(to bottom, rgba(49,130,183,1) 0%, rgba(49,130,183,1) 50%, rgba(0,0,0,0) 50%)`;
            }
            if (variables.state_empty) {
              return 'linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_done) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_auto) {
              return 'linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_used) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_clean) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_dirty) {
              return 'linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_charging) {
              return 'linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_discharging) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_unlocked) {
              return 'linear-gradient(to bottom, rgba(255,0,0,1) 0%, rgba(255,0,0,1) 50%, rgba(0,0,0,0) 50%)';
            }
            if (variables.state_locked) {
              return 'linear-gradient(to bottom, rgba(50,168,82,1) 0%, rgba(50,168,82,1) 50%, rgba(0,0,0,0) 50%)';
            } else {
              return 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0) 50%)';
            }
          ]]]
      - overflow: hidden
      - border-radius: 10px
      - border-width: 0px
      - -webkit-tap-highlight-color: rgba(0,0,0,0)
      - transition: none
      - --mdc-ripple-color: >
          [[[
            return variables.state_on
                ? 'var(--contrast1)'
                : '#97989c';
          ]]]
      - color: >
          [[[
            return variables.state_on
                ? 'var(--contrast1)'
                : '#97989c';
          ]]]
      - background-color: >
          [[[
            return variables.state_on
                ? 'var(--contrast20)'
                : 'rgba(115, 115, 115, 0.25)';
          ]]]
  custom_fields:
    circle: >
      [[[ 
        const timerEntity = variables.timer_entity;
        if (!timerEntity) return '';
        
        const timerState = states[timerEntity]?.state;
        return timerState === 'active' ? 'circle_cd' : 'circle_info';
      ]]]

There is alot here I know (I used this thread and have gone from there), but I am fighing with the circle custom_field. I have multiple timers that I want to see on the cards when they are active. I created the circle_cd template that incorporates another custom:button-card to display the countdown. I have circle_info for other information displayed on the cards. I call the two templates as necessary. The code is below.

The issue I am having is when both are included as a template. I have a template in the base file that I was hoping would choose between the two templates based on whether timer_entity is active. If I have one or the other, the templates work as expected. When I have both called out, it does not switch to circle_cd when the timer becomes active.

circle_cd:
  styles:
    custom_fields:
      circle:
        - width: 88%
        - margin: -3% 2% 0 0
        - justify-self: end
        - opacity: 1
  custom_fields:
    circle:
      card:
        type: custom:button-card
        entity: "[[[ return variables.timer_entity; ]]]"
        aspect_ratio: 1/1
        show_icon: false
        show_name: false
        show_state: true
        styles:
          card:
            - border: none
            - background: none
            - background-color: none
            - display: >
                [[[ 
                  return states[variables.timer_entity]?.state === 'active' ? 'block' : 'none'; 
                ]]]
          state:
            - font-size: 14px
            - font-weight: 700
            - color: >
                [[[
                  return variables.state_on
                      ? 'var(--contrast1)'
                      : '#97989c';
                ]]]

circle_info:
  styles:
    card:
      - --c-stroke-color-on: var(--contrast1)
      - --c-stroke-color-off: none
      - --c-stroke-color-state-off: "#97989c"
      - --c-fill-color-on: none
      - --c-fill-color-off: none
      - --c-stroke-width: 2.3
      - --c-font-color-on: var(--contrast1)
      - --c-font-color-off: none
      - --c-font-color-state-off: "#97989c"
      - --c-font-size: 14px
      - --c-unit-font-size: 10.5px
      - --c-font-weight: 700
      - --c-letter-spacing: -0.02rem
    custom_fields:
      circle:
        - width: 88%
        - margin: -3% 2% 0 0
        - justify-self: end
        - opacity: 1
  custom_fields:
    circle: >
      [[[ 
        const input = variables.circle_input;

        if (input === null) {
          return '';
        }

        let r = 22, c = r * 2 * Math.PI;
        let domain = entity.entity_id.split('.')[0],
            state = variables.state,
            unit = variables.circle_input_unit || ' ';

        const circle = (percentage, displayText, unit, isDisplay = false) => {
            let strokeColor = 'var(--c-stroke-color-on)';
            let fontColor = 'var(--c-font-color-on)';
            
            if (isDisplay && !variables.state_on) {
                strokeColor = 'var(--c-stroke-color-state-off)';
                fontColor = 'var(--c-font-color-state-off)';
            } else if (!variables.state_on && !isDisplay) {
                strokeColor = 'var(--c-stroke-color-off)';
                fontColor = 'var(--c-font-color-off)';
            }

            return `
              <svg viewBox="0 0 50 50">
                <style>
                  circle {
                    transform: rotate(-90deg);
                    transform-origin: 50% 50%;
                    stroke-dasharray: ${c};
                    stroke-dashoffset: ${c - percentage / 100 * c};
                    stroke-width: var(--c-stroke-width);
                    stroke: ${strokeColor};
                    fill: ${variables.state_on ? 'var(--c-fill-color-on)' : 'var(--c-fill-color-off)'}; 
                  }
                  text {
                    font-size: var(--c-font-size);
                    font-weight: var(--c-font-weight);
                    letter-spacing: var(--c-letter-spacing);
                    fill: ${fontColor}; 
                  }
                  tspan {
                    font-size: var(--c-unit-font-size);
                  }
                  #circle_value, tspan {
                    text-anchor: middle;
                    dominant-baseline: central;
                  }
                </style>
                <circle id="circle_stroke" cx="25" cy="25" r="${r}"/>
                <text id="circle_value" x="50%" y="52%">${displayText}${unit}</text>
              </svg>
            `;
        };

        const personIcon = (path, stateOn) => {
            let fillColor = stateOn ? 'var(--c-stroke-color-on)' : 'var(--c-stroke-color-state-off)';
            return `
              <svg viewBox="0 0 50 50">
                <style>
                  path {
                    fill: ${fillColor};
                    transform: scale(1.0);
                    transform-origin: center;
                  }
                </style>
                <path d="${path}"/>
              </svg>
            `;
        };

        if (domain === 'sensor' && entity.entity_id.includes('battery')) {
            const batteryLevel = parseFloat(input);
            return circle(batteryLevel, batteryLevel, unit, true);
        }

        if (domain === 'light' && state === 'on') {
            const brightness = entity.attributes.brightness;
            const percentage = brightness ? Math.round((brightness / 255) * 100) : 100;
            return circle(percentage, percentage, '%');
        }

        if (domain === 'fan') {
            if (state === 'on') {
                const percentage = entity.attributes.percentage;
                let displayText = '';
                if (percentage === 33) displayText = 'LOW';
                else if (percentage === 66) displayText = 'MED';
                else if (percentage === 100) displayText = 'HIGH';
                else displayText = 'ON';
                return circle(percentage, displayText, '');
            }
        }

        if (domain === 'climate') {
            let displayText = `${input}`;
            return circle(100, displayText, unit, true);
        }

        if (domain === 'binary_sensor') {
            let time = c => {
                let s = (c / 1e3),
                    m = (c / 6e4),
                    h = (c / 36e5),
                    d = (c / 864e5);
                return s < 60
                    ? parseInt(s) + 's'
                    : m < 60 ? parseInt(m) + 'm'
                    : h < 24 ? parseInt(h) + 'h'
                    : parseInt(d) + 'd';
            };
            let timeValue = states[variables.retain] === undefined || states[variables.retain].state === 'unavailable'
                    ? time(Date.now() - Date.parse(entity.last_changed))
                    : time(Date.now() - Date.parse(states[variables.retain].state));
            return circle(100, timeValue, ' ');
        }

        if (domain === 'person') {
            const paths = {
              'home': 'M22 37.75V28.75H28V37.75H35.5V25.75H40L25 12.25 10 25.75H14.5V37.75H22Z',
              'Friendship': 'M25 11.5 8.5 20.5 25 29.5 38.5 22.135V32.5H41.5V20.5M14.5 26.77V32.77L25 38.5 35.5 32.77V26.77L25 32.5 14.5 26.77Z',
              'Stantec': 'M22 10.75H28A3 3 0 0 1 31 13.75V16.75H37A3 3 0 0 1 40 19.75V36.25A3 3 0 0 1 37 39.25H13C11.335 39.25 10 37.9 10 36.25V19.75C10 18.085 11.335 16.75 13 16.75H19V13.75C19 12.085 20.335 10.75 22 10.75M28 16.75V13.75H22V16.75H28Z',
              'Kelly': 'M25 11.5 11.5 19.3V38.5H20.5L24.85 34 29.5 38.5H38.5V19.3L25 11.5M18.85 37V28L23.35 32.5 18.85 37M20.35 26.5H29.35L24.85 31 20.35 26.5M30.85 37 26.35 32.5 30.85 28V37M29.5 23.5H20.2V20.5H29.5V23.5Z',
              'Flinner': 'M25 29.5A3 3 0 0 1 22 26.5C22 24.835 23.35 23.5 25 23.5A3 3 0 0 1 28 26.5 3 3 0 0 1 25 29.5M17.5 17.5C17.5 15.835 18.835 14.5 20.5 14.5A3 3 0 0 1 23.5 17.5 3 3 0 0 1 20.5 20.5C18.835 20.5 17.5 19.15 17.5 17.5M25 10C19.645 10 14.845 12.31 11.5 16L25 40 38.5 16C35.17 12.31 30.355 10 25 10Z',
              'TPA': 'M37.855 29.29 40 27.145 37.855 25 32.5 30.355 19.645 17.5 25 12.145 22.855 10 20.71 12.145 18.565 10 15.355 13.21 13.21 11.065 11.065 13.21 13.21 15.355 10 18.565 12.145 20.71 10 22.855 12.145 25 17.5 19.645 30.355 32.5 25 37.855 27.145 40 29.29 37.855 31.435 40 34.645 36.79 36.79 38.935 38.935 36.79 36.79 34.645 40 31.435 37.855 29.29Z',
              'SVS': 'M25 11.5 8.5 20.5 25 29.5 38.5 22.135V32.5H41.5V20.5M14.5 26.77V32.77L25 38.5 35.5 32.77V26.77L25 32.5 14.5 26.77Z',
              'not_home': 'M42.25 27.25 36.25 33.25V28.75H22.75V25.75H36.25V21.25L42.25 27.25M12.25 37.75V25.75H7.75L22.75 12.25 33.25 21.7V22.75H29.935L22.75 16.285 15.25 23.035V34.75H30.25V31.75H33.25V37.75H12.25Z'
            };
            let path = paths[entity.state];
            return `
              <svg viewBox="0 0 50 50">
                ${circle(100, '', '', true)}
                ${personIcon(path, variables.state_on)}
              </svg>
            `;
        }

        if (variables.state_on) {
            return circle(100, input, unit);
        }
      ]]]

Any help would be greatly appreciated. Is what I am trying to do even achievable?

I am looking to improve UX for my car charger toggles. I have a button, that toggles my car charger.

type: custom:button-card
entity: switch.car_charger_1_charge_control
size: 20%
styles:
  name:
    - font-size: .8rem
    - opacity: 0.75
  label:
    - font-size: .8rem
    - white-space: nowrap
  icon:
    - height: 75%
  grid:
    - grid-template-areas: "\"n\" \"i\" \"l\" \"s\""
triggers_update: switch.car_charger_1_charge_control
name: "Car Charger #1"
label: |
  [[[
    return `${states['sensor.car_charger_1_power_active_import'].state} kW`;
  ]]]
show_label: true
hold_action:
  action: more-info
card_mod:
  style: |
    ha-card {
      height: 100%
    }    

When pressing it, it takes 3-10 seconds for the charger to turn on. Sometimes it’s not clear whether I successfully tapped the button or accidentally dragged my finger so the tap didn’t happen.

I would like to disable the button and apply some CSS to make it very clear that the button press happened. How can I change CSS in tap_action? In Javascript I’d do something like:

const button = document.querySelector('#your-button-id');
let isDisabled = false;

button.addEventListener('click', () => {
  // Prevent action if button is disabled
  if (isDisabled) return;
  
  // Disable the button
  isDisabled = true;
  button.disabled = true;
  button.classList.add('disabled');  // Add disabled styling
  
  // Toggle the car charger here
  
  // Re-enable after 1 second
  setTimeout(() => {
    isDisabled = false;
    button.disabled = false;
    button.classList.remove('disabled');
  }, 1000);
});

If anyone is interested here is the code for multple button cards nested in a picture elements card. Simple Air Comfort Card.

It has multiple bits of code that may be of interest to you and customising button card.

V4.0 Simple Air Comfort Card*

Updates with the help of Chat GPT
It took 4 days but I can highly recommend AI to help especially if you are like me and are self taught with yaml.

New Yaml for Floating Circle

  • Scales the Floating Circle perfectly with change in card size. No need for different setups. If you want to change the size of the Floating Circle, stipulate the same height: and width:
  • Floating Circle movement maintains its position no matter how the card is scaled.
  • Added variables so you can adjust your Outer Ring Limits and Inner Circle Comfort Zone.
  • Floating Circle now blinks red if outside Temp and Humidity limits are exceeded (could be changed for a mould sensor)

New Yaml for Inner Circle Comfort Zone

  • Red Ring appears when outside Temp and Humidity limits are exceeded.

Updated Code for Inner Circle Comfort Zone and Floating Circle

                      - type: custom:button-card
                        name: Inner Circle Comfort Zone
                        show_name: false
                        show_icon: false
                        aspect_ratio: 1/1
                        entity: sensor.text_dewpoint_comfort_airport
                        style:
                          right: 28.5%
                          top: 50%
                          height: 21%
                          width: 21%
                        styles:
                          card:
                            - border-radius: 50%
                            - border-width: 0px
                        card_mod:
                          style: |
                            ha-card {
                              {% set humidity_float = states('sensor.devonport_airport_humidity') | float(0) %}
                              {% set temperature_float = states('sensor.devonport_airport_temp') | float(0) %}
                              
                              {% set temp_min = 17 %}  {# 17°C = bottom of Outer Ring Limits #}
                              {% set temp_max = 30 %}  {# 30°C = top of Outer Ring Limits #}
                              {% set humidity_min = 40 %}  {# 40% = left of Outer Ring Limits #}
                              {% set humidity_max = 60 %}  {# 60% = right of Outer Ring Limits #}
                              
                              {# Check if humidity or temperature is outside of limits #}
                              {% set outside_limits = (humidity_float < humidity_min)