Home Assistant card (replacing former bar-card config)

Hello,
Thanks for your exemples .
A solution full button-card inspired from your code

The template :

button_card_templates:
  ts_bar_card:
    variables:
      _BaseName: baseName
      _sensor: null
      _maxSensor: null
      _maxLimit: -1
      _colorBase: grey
      _color1: SeaGreen
      _color50: SandyBrown
      _color75: Tomato
      _color90: red
      _sensorValue: >-
        [[[var val='';try{val = states[variables._sensor].state;}catch(e){val =
        -1;}return val; ]]]
      _maxSensorValue: >-
        [[[var val='';try{val = states[variables._maxSensor].state;}catch(e){
        try{val = variables._maxLimit;}catch(e){val =-1;}}return val; ]]]
      _z_FormatedTitle: |-
        [[[
            var base = variables._BaseName;
            var val = variables._sensorValue;
            var max = variables._maxSensorValue;
            var unit = states[variables._sensor].attributes.unit_of_measurement;
            var Lspan = "<span style=\"float:left;\">";
            var Rspan = "<span style=\"float:right;margin-right:5px;color:#dddddd;\">";
            var smallText = "<span style=\"font-size:0.75em;font-weight:500;\">"
            var cl = "</span>";
            var _t = Lspan+base+cl+Rspan+val+" "+unit+smallText+" / "+max+" "+unit+cl+cl;
            return _t;
        ]]]
    name: '[[[return variables._z_FormatedTitle;]]]'
    entity: '[[[return variables._sensor;]]]'
    triggers_update: '[[[return variables._sensor;]]]'
    show_state: false
    styles:
      grid:
        - grid-template-areas: '"i n"'
        - grid-template-rows: 1fr
        - grid-template-columns: 50px 1fr
      name:
        - width: 100%
        - color: white
        - font-weight: 500
      card:
        - color: lightgrey
        - font-size: 1.1em
        - height: 26px
        - padding: 0px 4px
        - border-radius: 8px
        - '--card-mod-icon-color': white
        - '--mdc-icon-size': 25px
        - background: |-
            [[[
              var v=variables;
              var perc = Math.round((v._sensorValue / v._maxSensorValue)*100.0);
              var rest = 100-perc;
              var bar = v._colorBase;
              if (perc >= 80 ){bar = v._color90 ;}
              else if (perc >= 60 ){bar = v._color50 ;}
              else if (perc >= 49 ){bar = v._color50;}
              else if (perc >= 1 ){bar = v._color1;}
              var ramp = 'linear-gradient(to right, rgba(from '+bar+' r g b / 0.9) 0%, rgb(from '+bar+' r g b /0.6) '+perc+'%, rgba(from '+bar+' r g b /0.3)'+perc+'%, rgba(from '+bar+' r g b /0.1) 100%)';
              return ramp;
            ]]] 

An exemple of card:

type: custom:button-card
template: ts_bar_card
variables:
  _BaseName: HA Database
  _sensor: sensor.home_assistant_v2_db_taille
  _maxSensor: null
  _maxLimit: 500
icon: mdi:database-outline
2 Likes

Do you have a screen shot? Picture says a thousand words.

seems that would do this:

guess that limit is a bit low…

upping that a bit returns:

so yeah that is nice indeed. will have to check that for actual percentages scaling up to 100

    - type: custom:button-card
      template: ts_bar_card
      variables:

        _sensor: sensor.alarm_button_hal_battery
        _maxSensor: null
        _maxLimit: 100

took out the name, as I want auto-entities to populate that, but that doesnt work
Scherm­afbeelding 2025-06-30 om 09.53.19

so a bit more work to do on that template for me,

add;

  variables:
    _BaseName: >
      [[[ return entity.attributes.friendly_name; ]]]

Also the max 100% isnt really useful, so that could be taken out.

Scherm­afbeelding 2025-06-30 om 10.05.19

small update

using my original color styling for the batteries. This can probably made much better in JS, but a quick and dirty rewrite at least works for now:

      - background: |-
          [[[
            const perc = parseFloat(entity.state);

            let bar;
            if (perc <= 10) { bar = '255,0,0'; }
            else if (perc <= 20) { bar = '128,0,0'; }
            else if (perc <= 30) { bar = '255,140,0'; }
            else if (perc <= 40) { bar = '255,165,0'; }
            else if (perc <= 50) { bar = '255,255,0'; }
            else if (perc <= 60) { bar = '154,205,5'; }
            else if (perc <= 70) { bar = '144,238,144'; }
            else if (perc <= 80) { bar = '50,205,50'; }
            else if (perc <= 90) { bar = '0,128,0'; }
            else { bar = '0,100,0'; }

            const [r, g, b] = bar.split(',').map(Number);
            return `linear-gradient(to right, rgba(${r},${g},${b},0.9) 0%, rgba(${r},${g},${b},0.6) ${perc}%, rgba(${r},${g},${b},0.3) ${perc}%, rgba(${r},${g},${b},0.1) 100%)`;
          ]]]

config in the dashboard still verbosely typed cards, not yet auto_entities):

  cards:
    - type: custom:button-card
      template: ts_bar_card
      variables:
        _sensor: sensor.alarm_button_battery
    - type: custom:button-card
      template: ts_bar_card
      variables:
        _sensor: sensor.alarm_button_hal_battery
    - type: custom:button-card
      template: ts_bar_card
      variables:
        _sensor: sensor.voordeur_sensor_battery

update 2

optimized javascript template for the background

      - background: |-
          [[[
            const perc = parseFloat(entity.state);

            const colorMap = [
              { threshold: 10,  color: '255,0,0' },
              { threshold: 20,  color: '128,0,0' },
              { threshold: 30,  color: '255,140,0' },
              { threshold: 40,  color: '255,165,0' },
              { threshold: 50,  color: '255,255,0' },
              { threshold: 60,  color: '154,205,5' },
              { threshold: 70,  color: '144,238,144' },
              { threshold: 80,  color: '50,205,50' },
              { threshold: 90,  color: '0,128,0' },
            ];

            const current = colorMap.find(entry => perc <= entry.threshold);
            const bar = current ? current.color : '0,100,0';

            return `linear-gradient(to right,
              rgba(${bar},0.9) 0%,
              rgba(${bar},0.6) ${perc}%,
              rgba(${bar},0.3) ${perc}%,
              rgba(${bar},0.1) 100%)`;
          ]]]
1 Like

Making this card-mod latch with the auto-entities as a ‘filter: template:’ is a challenge. I have it working in auto entities card but not with ‘filter: template:’

I will have a fight with Chat GPT and see if I can wrestle it into submission.

@Mariusthvdb that is quite a large Database you have there I try keep mine under 400Mb, closer to 300Mb, but I am running an Odroid N2+. You must have some beefy hardware running HA.

Current System Snapshot.

yeah, tbh, I was rather surprised too… seems to have to do with a very large update frequency of power/energy sensors

1 Like

Visually being able to see it on the dashboards helps to refine excludes from the logger. I keep 365 days worth of data when performing purges, ensuring I have my rain data for a year. But perform purge.entities quite often and wipe 365 days worth of data I do not need.

simplified that template a bit, it uses way too many stuff that shouldn’t be there, or can be optimized inside button-card itself:

ts_bar_card:

  name: '[[[return entity.attributes.friendly_name;]]]'
  entity: '[[[return entity;]]]'

  show_state: true
  show_icon: true
  styles:
    grid:
      - grid-template-areas: '"i n"'
      - grid-template-rows: 1fr
      - grid-template-columns: 50px 1fr
    name:
      - width: 100%
      - color: white
      - font-weight: 500
      - text-align: left
    icon:
      - color: black

    card:
      - color: lightgrey
      - font-size: 1.1em
      - height: 26px
      - padding: 14px
      - border-radius: 8px
      - background: |-
          [[[
            const perc = parseFloat(entity.state);

            const colorMap = [
              { threshold: 10,  color: '255,0,0' },
              { threshold: 20,  color: '128,0,0' },
              { threshold: 30,  color: '255,140,0' },
              { threshold: 40,  color: '255,165,0' },
              { threshold: 50,  color: '255,255,0' },
              { threshold: 60,  color: '154,205,5' },
              { threshold: 70,  color: '144,238,144' },
              { threshold: 80,  color: '50,205,50' },
              { threshold: 90,  color: '0,128,0' },
            ];

            const current = colorMap.find(entry => perc <= entry.threshold);
            const bar = current ? current.color : '0,100,0';

            return `linear-gradient(to right,
              rgba(${bar},0.9) 0%,
              rgba(${bar},0.6) ${perc}%,
              rgba(${bar},0.3) ${perc}%,
              rgba(${bar},0.1) 100%)`;
          ]]]

only seem to have lost my icons :wink:

also this works in auto-entities

    - type: custom:auto-entities
      card:
        type: vertical-stack
        title: Auto-entities Batteries
      card_param: cards
      filter:
        include:
          - entity_id: sensor.*battery_level
            options:
              type: custom:button-card
              template: ts_bar_card

off topic here, but yes, I have a tightly curated logger setup.

Unfortunately, I also have a lot of power entities, that get updated multiple times per second, and rely on each other. My smart meter is very active…
the ‘total’ sensors are the culprit there…

detailed:

resulting in that size:

looking at that, with the beautiful bars, it isnt completely 100% off topic after all :wink:

1 Like

I am a fair bit lower.

Culled to 365 days worth of data.

State Attributes make up most of my database size.

My most active event is localtuya devices but takes up bugger all room.

so final version for now, and much simplified, with a sleek trim function to delete repeating name strings:

ts_bar_card:
  name: >
    [[[ function removeSubstrings(str, subs) {
          subs.forEach(s => str = str.replace(new RegExp(s, 'gi'), ''));
          return str.trim();
        }
        return removeSubstrings(entity.attributes.friendly_name,
          ['sensor', 'level', 'battery', 'batterij', ':']); ]]]
  show_state: true
  styles:
    grid:
      - grid-template-areas: '"i n s"'
      - grid-template-columns: 50px 1fr
    name:
      - width: 100%
      - color: white
      - font-weight: 400
      - font-size: 16px

      - text-align: left
    card:
      - color: black # for elements other than icon and name
      - height: 26px
      - padding: 4px
      - font-size: 1em
      - border: 2px solid var(--ha-color);
      - border-radius: 8px
      - background: |-
          [[[
            const perc = parseFloat(entity.state);

            const colorMap = [
              {threshold: 10, color: '255,0,0'},
              {threshold: 20, color: '128,0,0'},
              {threshold: 30, color: '255,140,0'},
              {threshold: 40, color: '255,165,0'},
              {threshold: 50, color: '255,255,0'},
              {threshold: 60, color: '154,205,5'},
              {threshold: 70, color: '144,238,144'},
              {threshold: 80, color: '50,205,50'},
              {threshold: 90, color: '0,128,0'},
            ];

            const current = colorMap.find(entry => perc <= entry.threshold);
            const bar = current ? current.color : '0,100,0';

            return `linear-gradient(to right,
              rgba(${bar},0.9) 0%,
              rgba(${bar},0.6) ${perc}%,
              rgba(${bar},0.3) ${perc}%,
              rgba(${bar},0.1) 100%)`;
          ]]]

config in frontend:

  cards:
    - type: custom:button-card
      template: ts_bar_card
      entity: sensor.alarm_button_battery
    - type: custom:button-card
      template: ts_bar_card
      entity: sensor.alarm_button_hal_battery
    - type: custom:button-card
      template: ts_bar_card
      entity: sensor.voordeur_sensor_battery

    - type: entities
      title: Auto-entities Batteries
      card_mod:
        class: class-header-margin
        style: |
          ha-card {
            background: transparent;
          }
          .card-content {
            max-height: {{states('input_number.max_scroll_hoogte')}}px;
            overflow-y: scroll;
            padding: 0;
          }
      entities:
        - type: custom:auto-entities
          card:
            type: vertical-stack
          card_param: cards
          filter:
            exclude:
              - state: unknown
            include:
              - entity_id: sensor.*battery_level
                options:
                  type: custom:button-card
                  template: ts_bar_card
          sort:
            method: state
            numeric: true
            reverse: true

stand alone, or in entities with auto-filled vertical-stack. can also do vertical-stack as primary card of course, like the post above

Iers difficult to find good generic colors for all %s as the contrast with the background is so variable. Black seems to do well (see the percentage state). White (for the name) is such more difficult.
Anyways, preferences may vary there

removed the padding from the vertical stack
Jun-30-2025 14-19-01

and reverse: true
Jun-30-2025 14-19-24

should give enough options to play with now…

2 Likes

Did you try the colour transitions I come up with? It is a delicate balance of icon contrast, text readability and background colour. Unlike the transition I tweaked.

no? got a link?

Hi @Mariusthvdb Nice template for button card… :clap:

I recently created something similar for my custom card. so let me improve your template. I modified the function so that the background gradient width is in the range of the maximum entity current value, instead of the entire 100% card width. I created two versions, one with a color gradient and the other with color blocks. Maybe someone prefers this style more.

2025-07-01 23.35.21

base card template:

ts_bar_card:
  show_state: true
  styles:
    grid:
      - grid-template-areas: '"i n s"'
      - grid-template-columns: 50px 1fr
    name:
      - width: 100%
      - color: white
      - font-weight: 400
      - font-size: 16px
      - text-align: left
      - z-index: 2
    state:
      - z-index: 2
    icon:
      - z-index: 2
    card:
      - height: 26px
      - padding: 4px 8px
      - font-size: 1em
      - border-radius: 8px
      - position: relative
      - background: var(--secondary-background-color)

gradient

  - type: custom:button-card
    entity: input_number.test_max_value
    name: Color Gradient
    template:
      - ts_bar_card
      - color_gradient_bar
color_gradient_bar:
  styles:
    custom_fields:
      bar:
        - position: absolute
        - top: 0
        - left: 0
        - bottom: 0
        - width: 100%
        - height: 100%
        - border-radius: 8px
        - z-index: 1
        - transition: width 0.5s ease-out
        - background: >
            [[[
              let currentLevel = parseFloat(entity.state),
                  maxValue = variables.max_value || 100;
              function generateGradient() {
                const colorThresholds = [
                  { "value": 0, "color": "#00ff4c"},
                  { "value": 20, "color": "#d49adf" },
                  { "value": 40, "color": "#00d5ff"},
                  { "value": 60, "color": "#850000"},
                  {"value": 80,"color": "#342678"},
                  {"value": 90,"color": "#7968cf"}
                ]
                const sorted = colorThresholds.sort((a, b) => a.value - b.value);
                const _level = Math.min(currentLevel, maxValue);

                let stops = [];
                let lastColor = sorted[0].color;

                for (const { value, color } of sorted) {
                  if (value > _level) break;
                  const percentage = (value / _level) * 100;
                  stops.push(`${color} ${percentage}%`);
                  lastColor = color;
                }

                const normalizedLevel = 100;
                const lastStop = stops[stops.length - 1];
                if (!lastStop?.endsWith(`${normalizedLevel}%`)) {
                  stops.push(`${lastColor} ${normalizedLevel}%`);
                }

                return `linear-gradient(90deg, ${stops.join(', ')})`;
              }
              return generateGradient();
            ]]]
  custom_fields:
    bar: >
      [[[
        let currentLevel = parseFloat(entity.state);
        setTimeout(() => {
          let elt = this.shadowRoot,
              card = elt.getElementById('card'),
              container = elt.getElementById('container'),
              bar = elt.getElementById('bar');
          if (elt && card && container && bar) {
            card.insertBefore(bar, container);
            bar.style.width = currentLevel + '%';
          }
        }, 0);
        return ' ';
      ]]]

color blocks

  - type: custom:button-card
    entity: input_number.test_max_value
    name: Color Block
    template:
      - ts_bar_card
      - color_block_bar
color_block_bar:
  styles:
    custom_fields:
      bar:
        - position: absolute
        - top: 0
        - left: 0
        - bottom: 0
        - width: 100%
        - height: 100%
        - border-radius: 8px
        - z-index: 1
        - transition: width 0.5s ease-out
        - background: >
            [[[
              let currentLevel = parseFloat(entity.state),
                  maxValue = variables.max_value || 100;
              function generateColorBlocks() {
                const colorThresholds = [
                  { "value": 0, "color": "#00ff4c"},
                  { "value": 20, "color": "#d49adf" },
                  { "value": 40, "color": "#00d5ff"},
                  { "value": 60, "color": "#850000"},
                  {"value": 80,"color": "#342678"},
                  {"value": 90,"color": "#7968cf"}
                ]
                const sorted = colorThresholds.sort((a, b) => a.value - b.value);
                const cappedLevel = Math.min(currentLevel, maxValue);

                let stops = [];

                for (let i = 0; i < sorted.length - 1; i++) {
                  const current = sorted[i];
                  const next = sorted[i + 1];

                  if (current.value > cappedLevel) break;

                  const fromValue = (current.value / cappedLevel) * 100;
                  const toValue = (Math.min(next.value, cappedLevel) / cappedLevel) * 100;

                  stops.push(`${current.color} ${fromValue}%`, `${current.color} ${toValue}%`);

                  if (next.value >= cappedLevel) break;
                }

                let lastThreshold;

                for (let i = sorted.length - 1; i >= 0; i--) {
                  if (sorted[i].value <= cappedLevel) {
                    lastThreshold = sorted[i];
                    break;
                  }
                }
                if (lastThreshold && lastThreshold.value < cappedLevel) {
                  const start = (lastThreshold.value / cappedLevel) * 100;
                  stops.push(`${lastThreshold.color} ${start}%`, `${lastThreshold.color} 100%`);
                }

                return `linear-gradient(90deg, ${stops.join(', ')})`;

              }
              return generateColorBlocks();
            ]]]
  custom_fields:
    bar: >
      [[[
        let currentLevel = parseFloat(entity.state);
        setTimeout(() => {
          let elt = this.shadowRoot,
              card = elt.getElementById('card'),
              container = elt.getElementById('container'),
              bar = elt.getElementById('bar');
          if (elt && card && container && bar) {
            card.insertBefore(bar, container);
            bar.style.width = currentLevel + '%';
          }
        }, 0);
        return ' ';
      ]]]

bars in entities card
2025-07-01 23.44.05

bars in entities card
  - type: entities
    entities:
      - entity: input_number.test_max_value
      - type: custom:button-card
        entity: input_number.test_max_value
        name: Color Gradient
        template:
          - ts_bar_card
          - color_gradient_bar
      - type: custom:button-card
        entity: input_number.test_max_value
        name: Color Block
        template:
          - ts_bar_card
          - color_block_bar
    state_color: true
1 Like

haha, wow.
and you use this where? to display what? really curious, as I could not find something to show like that in my config…

I do have a few lights that with the colorize option of slider-entity-row do something like that

Scherm­afbeelding 2025-07-01 om 23.59.02

but I need some inspiration from you to get me to test that template on an actual entity :wink:

1 Like

Here, I used a color threshold in my custom card … :grin:

I hadn’t played with the button card template for a long time, but I got into it and improved the template using variables for clarity and combined these functions.

2025-07-02 01.00.28

cards…

cards
# Anchors
color_gradient_bar_base: &color_gradient_bar
  type: custom:button-card
  entity: input_number.test_max_value
  name: Color Gradient
  template:
    - color_bars
color_block_bar_base: &color_block_bar
  type: custom:button-card
  entity: input_number.test_max_value
  name: Color Block
  variables:
    color_blocks: true
  template:
    - color_bars

cards:
  - type: entities
    title: Color Bars - default max 100
    entities:
      - entity: input_number.test_max_value
        name: default max value 100
      - *color_gradient_bar
      - *color_block_bar
      ## Entity custom max value
      - entity: number.small_range
        name: variable max value 255
      - <<: *color_gradient_bar
        entity: number.small_range
        variables:
          max_value: 255
          colors: max_255
      - <<: *color_block_bar
        entity: number.small_range
        variables:
          max_value: 255
          color_blocks: true
          colors: max_255

templates:

color stops variables
threshold_vars:
  variables:
    default_colors: >
      [[[
        return [
                  { "value": 0, "color": "#00ff4c"},
                  { "value": 20, "color": "#d49adf" },
                  { "value": 40, "color": "#00d5ff"},
                  { "value": 60, "color": "#850000"},
                  {"value": 80,"color": "#342678"},
                  {"value": 90,"color": "#7968cf"}
                ]
      ]]]
    max_255: >
      [[[
        return [
                  { "value": 0, "color": "#7968cf"},
                  { "value": 50, "color": "#342678" },
                  { "value": 100, "color": "#850000"},
                  { "value": 150, "color": "#00d5ff"},
                  { "value": 200, "color": "#d49adf"},
                  { "value": 250, "color": "#00ff4c"}
                ]
      ]]]

updated with custom max_value, styles variables option…

color_bars:
  template:
    - ts_bar_card
    - threshold_vars
  variables:
    color_blocks: >
      [[[
        return false;
      ]]]
  custom_fields:
    bar: >
      [[[
        let currentLevel = parseFloat(entity.state);
        setTimeout(() => {
          let elt = this.shadowRoot,
              card = elt.getElementById('card'),
              container = elt.getElementById('container'),
              bar = elt.getElementById('bar');
          if (elt && card && container && bar) {
            card.insertBefore(bar, container);
            // if max value is set, calculate width with scaled percentage
            let maxValue = variables.max_value || 100;
            let scaledLevel = (currentLevel / maxValue) * 100;
            bar.style.width = scaledLevel + '%';;
          }
        }, 0);
        return ' ';
      ]]]
  styles:
    custom_fields:
      bar:
        - position: absolute
        - top: 0
        - left: 0
        - bottom: 0
        - width: 100%
        - height: 100%
        - border-radius: 8px
        - z-index: 1
        - transition: width 0.5s ease-out
        - background: >
            [[[
              let currentLevel = parseFloat(entity.state),
                  maxValue = variables.max_value || 100,
                  colorThresholds = variables.colors ?
                    variables[variables.colors] :
                    variables.default_colors;
              // Function to generate color blocks based on thresholds
              function generateColorBlocks() {
                const sorted = colorThresholds.sort((a, b) => a.value - b.value);
                const cappedLevel = Math.min(currentLevel, maxValue);

                let stops = [];

                for (let i = 0; i < sorted.length - 1; i++) {
                  const current = sorted[i];
                  const next = sorted[i + 1];

                  if (current.value > cappedLevel) break;

                  const fromValue = (current.value / cappedLevel) * 100;
                  const toValue = (Math.min(next.value, cappedLevel) / cappedLevel) * 100;

                  stops.push(`${current.color} ${fromValue}%`, `${current.color} ${toValue}%`);

                  if (next.value >= cappedLevel) break;
                }

                let lastThreshold;

                for (let i = sorted.length - 1; i >= 0; i--) {
                  if (sorted[i].value <= cappedLevel) {
                    lastThreshold = sorted[i];
                    break;
                  }
                }
                if (lastThreshold && lastThreshold.value < cappedLevel) {
                  const start = (lastThreshold.value / cappedLevel) * 100;
                  stops.push(`${lastThreshold.color} ${start}%`, `${lastThreshold.color} 100%`);
                }

                return `linear-gradient(90deg, ${stops.join(', ')})`;
              }

               // Function to generate the gradient
              function generateGradient() {
                const sorted = colorThresholds.sort((a, b) => a.value - b.value);
                const _level = Math.min(currentLevel, maxValue);

                let stops = [];
                let lastColor = sorted[0].color;

                for (const { value, color } of sorted) {
                  if (value > _level) break;
                  const percentage = (value / _level) * 100;
                  stops.push(`${color} ${percentage}%`);
                  lastColor = color;
                }

                const normalizedLevel = 100;
                const lastStop = stops[stops.length - 1];
                if (!lastStop?.endsWith(`${normalizedLevel}%`)) {
                  stops.push(`${lastColor} ${normalizedLevel}%`);
                }

                return `linear-gradient(90deg, ${stops.join(', ')})`;
              }
              return variables.color_blocks ? generateColorBlocks() : generateGradient();
            ]]]
1 Like

Sorry for the late reply.

I am still using custom:bar-card, actually.
There’s a fork that’s active: GitHub - spacerokk/bar-card: Customizable Animated Bar card for Home Assistant Lovelace .
Haven’t switched yet, since the original still works for me. But it’s good to know there’s an alternative in case the original breaks down.

1 Like

So what I’ve been wanting to do is very basic compared to waht has been done here, but this seems to be the only thing I can find that looks capable. Do you think this would work for what Im trying to do on a custom climate card.
https://www.reddit.com/r/homeassistant/comments/1lwe3b8/thermostat_card_iso/

was trying bar-card but it doesnt seem capable of what i want

There is someone that created that exact card on a topic in here