o heck, sorry…
I’ll await your releases for 2025.5 then
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.
will do
I just mentioned you here 🗄️ Sidebar Organizer - #18 by Mariusthvdb give it a thought?
I,ve psted my findings.
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
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:
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.
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()
})
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!
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?
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 like this:
(blurred cam feed)
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!