2025.5 theme variable changes (alert)

o heck, sorry…
I’ll await your releases for 2025.5 then :wink:

You can use the kiosk-mode.js bundle inside the dist folder in the pull request. The beta version is for template support in Kiosk Mode.

1 Like

will do
I just mentioned you here 🗄️ Sidebar Organizer - #18 by Mariusthvdb give it a thought?

1 Like

I,ve psted my findings. :wink:

Just for clarification: app-header-text-color colours all tabs inc. the active one, whereas sl-color-neutral-600 colours all inactive tabs.

Or did Iget you wrong?

thanks and yes can confirm all of the hides are working again!
Seems ready for release :wink:

1 Like

no, not wrong, I believe that is what I found yes. It makes you believe that app-header-text-color is for the active tab, but in fact is is the sl-color-neutral-600 overriding that everywhere, except for the active one…

and, since sl-color-neutral-600 also overrides any custom colorization one sets in card_mod with

      sl-tab[aria-label='Welkom'] {
        --card-mod-icon: {{'mdi:home-alert' if alerts }};
        /*background: url('/local/devices/hue_home.png');*/
        color: {{'var(--alert-color)' if alerts else 'var(--success-color)'}};
      }

I rather not use that option, because it layers too many theme variables.

so, I set a generic tab color with the appropriate theme variable app-header-text-color everywhere, and override that for the active tab with the current card-mod. Or override it based on conditions as shown above.

as you can see overriding the active tab uses the new --sl-color-primary-600, while overriding any other tab simply uses color:

Fwiw, I have been able to mitigate/incorporate all changes in theming variables and new designs with the great help of @elchininet and his wonderful plugins: Kiosk-mode (by @NemesisRE), Custom-sidebar, Keep-texts-in-tabs, and several serious changes in the card-mod theming.

That is, under the premise of those plugins to be updated to release soon, I have been running their dev versions for a few days.

If your interested, you can find my solutions at

Custom-sidebar config
Card-mod-themes
Themes

the latter Themes doesnt hold many new settings, it is mainly the

    ha-badge-icon-size: 24px
    app-header-selection-bar-color: var(--active-color)

that allows us to do away with card-mod theming for those properties now. Of course, I’ve deleted all paper-xxx references.

Almost all of my sidebar settings are in the dedicated config file, except for some animations inside the sidebar, that require card-mod theming. It has several big changes compared to before.
if you need/want to compare, my previous files are also in the gist, with prefix pre-2025

FR for dedicated theme variables on the tab bar: FR: add theme variable for active tab icon/text color · home-assistant/frontend · Discussion #25333 · GitHub

Issue with the new scrolling badges caused by hidden badges Badge scroll does not recognize hidden badges and fades anyway · Issue #25343 · home-assistant/frontend · GitHub

Final result for now:

edit
Forgot: tabbed-card is still borked. And given the activity in that repo, tbh, I dont have a lot of hope that will be fixed anytime soon.
So I guess we need to look elsewhere for a viable replacement (or fork and fix)

For the time being I changed those to a grid with a heading per Floor Input_select in a Tile, and conditional picture glances based on the input_select… Dont think Ill go back to the tabbed-card, this is all in core and perfect

Tabbed card replacement
type: vertical-stack
card_mod:
  style: |
    div#root {
      row-gap: 0;
    }
cards:
  - type: tile
    features:
      - type: select-options
    features_position: inline
    vertical: false
    entity: input_select.utilities_tabs
    hide_state: true

  - type: conditional
    conditions:
      - condition: state
        entity: input_select.utilities_tabs
        state: Stookhok
    card: !include /config/dashboard/includes/plattegrond/include_tab_stookhok.yaml

  - type: conditional
    conditions:
      - condition: state
        entity: input_select.utilities_tabs
        state: Dorm
    card: !include /config/dashboard/includes/plattegrond/include_tab_dorm.yaml

  - type: conditional
    conditions:
      - condition: state
        entity: input_select.utilities_tabs
        state: Gang
    card: !include /config/dashboard/includes/plattegrond/include_tab_gang.yaml

  - type: conditional
    conditions:
      - condition: state
        entity: input_select.utilities_tabs
        state: Garage
    card: !include /config/dashboard/includes/plattegrond/include_tab_garage.yaml

in Motion:

May-08-2025 14-43-32

It seems the 2025.5 release has broken common documented element like

–state-binary_sensor-active-color
–state-binary_sensor-door-off-color
–state-binary_sensor-window-off-color
–state-binary_sensor-lock-off-color

They are correct in pop up dialogs. But wrong in simple basic cards like entity card.

What has happended to these?

I cannot see these listed in the release note.

They are untouched as far as I am aware and working properly

Maybe it’s the prefix dash you have there ?

I was using a js script to load the CSS

document.documentElement.style.setProperty(‘–state-binary_sensor-active-color’, ‘var(–red-color)’);

Did they change that syntax?

I moved all my css items to a normal theme instead and there it works so the method of loading a js file from configuration.yaml with the definitions does not work as before for some reason.
I liked that way because then I did not have to set a theme and it worked in all themes

Update

In case other have same problem. I cannot make loading css via js work. It was a method someone posted in the forum and I cannot say that broke it and it was not an official method.

I have made a theme like this

  ha-card-border-radius: 1px
  ha-view-sections-column-gap: 7px
  ha-view-sections-row-gap: 7px
  ha-view-sections-column-min-width: 250px
  ha-view-sections-column-max-width: 500px
  ha-section-grid-row-gap: 8px
  ha-section-grid-column-gap: 8px
  state-cover-active-color: var(--orange-color)
  state-cover-inactive-color: '#008000'
  state-alarm_control_panel-disarmed-color: "#008000"
  state-alarm_control_panel-armed_away-color: var(--red-color)
  state-binary_sensor-active-color: var(--red-color)
  state-binary_sensor-door-off-color: "#008000"
  state-binary_sensor-window-off-color: "#008000"
  state-binary_sensor-lock-off-color: "#008000"

The first ones are related to getting HA to display properly on an iPad. The extra white space added a few months back made 3 column view become 2 column.

But for colours the state… lines shows a working syntax for both using variables for colour and rgb values.

1 Like

Not sure, never used that technique. I made a remark on the prefix dash because thought it to be a single dash, and not the required double dash. My eyes probably deceived me?

Also, using a variety of themes, I find it very easy to use blocks of identical theme variables under a yaml anchor.

Lastly, it is the supported technique, and that is rather important to me.

Yeah but that is like it has always been? Nothing new under the sun there?

looks like 2025.5 also breaks the custom javascript resource i used to completely get rid of the sidebar border. previously, the script below would kick in a second or so after the page loaded and make the border disappear. now, the tiny little border stays (although it is admittedly subtle):

based on the deprecated tokens, i suspect the reason the js custom resource has stopped working has to do with the “paper-listbox” down toward the bottom, but not sure how to fix it. if i go to the inspect panel, i can fix it temporarily by just changing the background-color under .mdc-drawer to transparent, but i can’t figure out how to make it happen automatically using the root or sidebar card-mod options in my config file.

here’s the script that’s worked for a while (i found it in another thread a year or two back):


class CustomStyle {
  refs = {
    ha: null,
    main: null,
    drawer: null,
    drawer: null,
    sidebar: null,
    menu: null,
  }

  constructor() {
    try {
      this.refs.ha = document.querySelector("home-assistant")
      this.refs.main = this.refs.ha?.shadowRoot.querySelector("home-assistant-main")?.shadowRoot
      this.refs.drawer = this.refs.main?.querySelector("ha-drawer")?.shadowRoot
      this.refs.sidebar = this.refs.main?.querySelector("ha-sidebar")
      this.refs.menu = this.refs.sidebar?.shadowRoot.querySelectorAll(".menu")[0]
      this.run()
    } catch (ex) { }
  }

  run = () => {
    console.info(`%c  CUSTOM-STYLE-MODE IS LOADED  `, "color: #ff9800; font-weight: bold; background-color: black")
    setTimeout(() => {
	  try {
        this.refs.drawer.querySelector("aside").style.borderRightStyle = "unset"
	  } catch(error) { }
    }, 1)
  }
}

Promise.resolve(customElements.whenDefined("paper-listbox")).then(() => {
  window.customStyle = new CustomStyle()
})
1 Like

Your script doesn’t need many changes, just to change the name of an element. paper-listbox doesn’t exist anymore, so just change it by ha-md-list. And it is not needed a Promise.resolve because whenDefined already returns a promise.

customElements.whenDefined("ha-md-list").then(() => {
  window.customStyle = new CustomStyle();
});

yep that worked - my ocd is eternally grateful. thank you!

1 Like

still dont get why that would be better than using a theme with that one setting, which would be all. in core and require nothing custom (always best imho)
or, if you start using custom stuff, not simply use the ready made great custom-sidebar plugin?

1 Like

Thanks for the tips. I changed my header card-mod code to reflect the 2025.05 changes. But I haven’t figured out how to change the font size (Selected/Unselected). “font-size” doesn’t work anymore. I tried “–ha-font-size” unsuccessfully. Can you provide guidance? Thank you!

  header-height: 60px
  card-mod-root-yaml: |
      ha-tabs$: |
          #tabsContainer {
              display: flex;
              justify-content: left;
              padding-right: 5px;
          }
          #selectionBar {
              border-bottom: 3px solid;
              color: var(--paper-item-icon-active-color);
              }
      .: |
          sl-tab[aria-selected=true] {
              border: solid var(--paper-item-icon-active-color);
              box-shadow: 0px 0px 15px var(--paper-item-icon-active-color);
              background: radial-gradient(transparent 0%, var(--paper-item-icon-active-color) 150%);    
              --mdc-icon-size: 40px;
              font-size: 12px;    
          }
          sl-tab {
              height: 45px;          
              border-radius: 10px;  
              margin-top: 3px;
              margin-bottom: 3px;
              color: white;      
              }
          ha-tabs { 
              display: flex;
              justify-content: space-between !important;
              color: var(--paper-item-icon-active-color);  
              font-size: 12px;      
          }

Hey funny, i think that was my script… ive updated it also a little bit. Here is new updated version

elementsA: is an array where you can also add some styling injection but array can also be empty etc…

Think the script will explain itself. Complete code here:

class CustomStyle {
  refs = {
    homeAssistant: null,
    homeAssistantRoot: null,
    haDrawer: null,
    haSidebar: null,
    menu: null,
  };

  elementsA = [
    {
      selector: ["span.title > p.main"],      // required: querySelector
      match: "Hikvision",                     // optional: match this text in innerText or innerHTML
      onMatch: (node) => {
        const header = node.closest("ha-dialog-header");
        if (header) {
          this.injectScopedStyle(header, `
            @media screen and (min-width: 599px) {
              header {
                display: none !important;
              }
            }
          `, "__hikvision_header_style");
        }

        this.injectScopedStyle(node, `
          :host * {
            @media screen and (min-width: 599px) {
              --mdc-dialog-min-width: 50vw !important;
              --mdc-dialog-max-width: 65vw !important;
            }
          }
        `, "__hikvision_style");
      }
    },
    {
      selector: "#header__title > span",
      onMatch: (node) => {
        node.style.fontSize = "var(--card-title-font-size)";
        node.style.lineHeight = "var(--card-title-line-height)";
        node.style.fontWeight = "var(--card-title-font-weight)";
        node.style.color = "var(--primary-text-color)";
      }
    },
    {
      selector: ["inject custom css selector"],
      onMatch: (node) => {
        const style = document.createElement("style");
        style.textContent = `
          :host * { 
            /* css */ 
          }
        `;
        node.shadowRoot?.appendChild(style) || node.appendChild(style);
      }
    }
  ]

  constructor() {
    try {
      this.refs.homeAssistant = document.querySelector("home-assistant") ?? null;
      this.refs.homeAssistantRoot = this.refs.homeAssistant?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot ?? null;
      this.refs.haDrawer = this.refs.homeAssistantRoot?.querySelector("ha-drawer")?.shadowRoot ?? null;
      this.refs.haSidebar = this.refs.homeAssistantRoot?.querySelector("ha-sidebar") ?? null;
      this.refs.menu = this.refs.haSidebar?.shadowRoot?.querySelectorAll(".menu")[0] ?? null;
      this.setupMutationObserver();
      this.run();
    } catch (error) {
      console.error("Error initializing CustomStyle:", error);
    }
  }

  setupMutationObserver(){const observer=new MutationObserver((mutations)=>{for(const mutation of mutations){for(const node of mutation.addedNodes){if(node.nodeType===Node.ELEMENT_NODE||node.nodeType===Node.TEXT_NODE){requestAnimationFrame(()=>{this.traverseAndMatch(node)})}}}});const observeShadowTree=(element)=>{if(!element?.shadowRoot)return;observer.observe(element.shadowRoot,{childList:!0,subtree:!0,});element.shadowRoot.querySelectorAll("*").forEach((child)=>{if(child.shadowRoot){observeShadowTree(child)}})};observeShadowTree(this.refs.homeAssistant)}
  injectScopedStyle(node,css,key="__customStyleInjected"){if(!node||node[key])return;const style=document.createElement("style");style.textContent=css.trim();if(node.shadowRoot){node.shadowRoot.appendChild(style)}else{node.appendChild(style)} node[key]=!0}
  traverseAndMatch(root){const stack=[root];while(stack.length>0){const node=stack.pop();if(!(node instanceof Element))continue;for(const rule of this.elementsA){if(!rule.selector)continue;const selectors=Array.isArray(rule.selector)?rule.selector:[rule.selector];selectors.forEach((selector)=>{const matchedElements=node.querySelectorAll(selector);matchedElements.forEach((matchedElement)=>{if(!matchedElement)return;const content=(matchedElement.textContent||matchedElement.innerText||matchedElement.innerHTML||"").trim().toLowerCase();if(rule.match){const matchList=Array.isArray(rule.match)?rule.match:[rule.match];const matchFound=matchList.some((matchText)=>content.includes(matchText.toLowerCase()));if(matchFound){rule.onMatch?.(matchedElement)}}else{rule.onMatch?.(matchedElement)}})})}if(node.shadowRoot){stack.push(...Array.from(node.shadowRoot.children))}stack.push(...Array.from(node.children))}}

  run = () => {
    console.info(`%c  CUSTOM-STYLE-MODE IS LOADED  `, "color: #ff9800; font-weight: bold; background-color: black");
    setTimeout(() => {
      try {
        if (this.refs.haDrawer.querySelector("aside")) this.refs.haDrawer.querySelector("aside").style.borderRightStyle = "unset"        
        if (this.refs.haSidebar.shadowRoot.querySelector("ha-md-list")) {
          this.refs.haSidebar.shadowRoot.querySelector("ha-md-list").style.scrollbarColor = "auto"
          this.refs.haSidebar.shadowRoot.querySelector("ha-md-list").style.scrollbarWidth = "auto"

          const style = document.createElement("style");
          style.textContent = `
            ha-md-list-item { max-width: -webkit-fill-available !important; }
            ha-md-list-item.selected::before { left: 2px !important; }
            ha-md-list-item.selected { border-left: 2px solid var(--primary-color) !important; border-bottom-left-radius: 5px; border-top-left-radius: 5px; }
          `;
          this.refs.haSidebar.shadowRoot.querySelector("ha-md-list").appendChild(style);
        }
      } catch (error) {
        console.warn("CUSTOM-STYLE-MODE run() error:", error);
      }
    }, 1);
  };
}

// Run it when Home Assistant is ready
Promise.resolve(customElements.whenDefined("hui-view")).then(() => {
  window.customStyle = new CustomStyle();
});

setupMutationObserver
injectScopedStyle
traverseAndMatch

are minified, here those three functions:

  setupMutationObserver() {
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
            // Scan entire tree from this node recursively, including shadow roots
            requestAnimationFrame(() => {
              this.traverseAndMatch(node);
            });
          }
        }
      }
    });

    const observeShadowTree = (element) => {
      if (!element?.shadowRoot) return;

      observer.observe(element.shadowRoot, {
        childList: true,
        subtree: true,
      });

      // Recurse to observe deeper shadow roots
      element.shadowRoot.querySelectorAll("*").forEach((child) => {
        if (child.shadowRoot) {
          observeShadowTree(child);
        }
      });
    };

    observeShadowTree(this.refs.homeAssistant);
  }

  injectScopedStyle(node, css, key = "__customStyleInjected") {
    if (!node || node[key]) return;
    
    const style = document.createElement("style");
    style.textContent = css.trim();
  
    if (node.shadowRoot) {
      node.shadowRoot.appendChild(style);
    } else {
      node.appendChild(style);
    }
  
    node[key] = true;
  }

  // Traverse a node and run rule checks by querying selectors + (optional) innerText / innerHtml
  traverseAndMatch(root) {
    const stack = [root];

    while (stack.length > 0) {
      const node = stack.pop();

      if (!(node instanceof Element)) continue;

      for (const rule of this.elementsA) {
        if (!rule.selector) continue;

        const selectors = Array.isArray(rule.selector) ? rule.selector : [rule.selector];

        selectors.forEach((selector) => {
          const matchedElements = node.querySelectorAll(selector);

          matchedElements.forEach((matchedElement) => {
            if (!matchedElement) return;
        
            const content = (matchedElement.textContent || matchedElement.innerText || matchedElement.innerHTML || "").trim().toLowerCase();
        
            if (rule.match) {
              const matchList = Array.isArray(rule.match) ? rule.match : [rule.match];
              const matchFound = matchList.some((matchText) => content.includes(matchText.toLowerCase()));
      
              if (matchFound) {
                rule.onMatch?.(matchedElement);
              }
            } else {
              // No match string needed, just matching on selector
              rule.onMatch?.(matchedElement);
            }
          });
        });
      }

      // Recursively step into shadow DOM
      if (node.shadowRoot) {
        stack.push(...Array.from(node.shadowRoot.children));
      }

      // Recursively add all child elements
      stack.push(...Array.from(node.children));
    }
  }

something i did in first two elements in elementsA, it removed header and title from a camera popup, so i only get me security cam popup :slight_smile: like this:

(blurred cam feed)

1 Like

custom-sidebar is also working for me with new 2025.5…
and it was updated even before i updated to 2025.5… so all thanks to custom-sidebar developer!

1 Like