🔹 Card-mod - Super-charge your themes!

A while ago, I created card-mod, a custom card* that lets you change various css options for other lovelace cards.
Much more skilled people than I have used that to create beautiful and awesome things.

Today, I’ve added theme support to card-mod.
Your ordinary theme file can now be used to

  • Give cards a flashing border
  • Make that border only flash when a certain entity is in a certain state
  • Make fan icons spin when they are on
  • Change the border color of the badges at the top of the screen depending on their entity type
  • Make the header bar transparent
  • Change the position of the more-info-dialog
  • Display the battery charge of any entity that has a battery in the secondary-info line in entities cards
  • Further clarify the battery charge by means of a bar graph in the background of the same row.
  • Change the lovelace background if your user name is Bob
  • Hide the header toggle of all entities cards by default
  • Make all glance cards green, except the ones the user says should be pink
  • Move the badge row to the bottom of the view
  • Space out the cards a bit more, or squeeze them together
  • Add mouse-over effects to the rows in entities cards

and much much more. Really, the possibilities are far from endless, but much closer to it than ever before!

And all the end user will have to do is:

  • Install card-mod from HACS
  • Install your theme
  • Enable your theme

The documentation for those new features is still… somewhat lacking… but I just couldn’t wait to see what all the skilled theme creators could do with this.
I’ll gladly answer any questions and offer support, though.
There’s two demo themes available. Please see the topic below for more info:

* it’s not a card

6 Likes

This has tonnes of potential and will likely enable me to remove lots of existing visual mods.

Very much looking forward to the instructions!

Thanks for your hard work and sharing

Quick question, could you just give a basic example of what to put in a card to use your example themes?

Nothing. 

2 Likes

I don’t seem to be able to get classes to work (obviously I’m missing something).

My theme includes:

  card-mod-card: |
    ha-card.border {
      border: solid 1px '#5294E2';
    }

A simple test card that does not get a border:

type: entities
title: My Title
entities:
  - light.all_lights
  - light.all_bedroom_lights
  - light.all_deck_lights
class: border

Try it without the quotes around the color.

Still no border visible. I double checked I am running lovelace-card-mod v2.0

Is the theme working otherwise?
If not, have you set the card-mod-theme variable to the name of the theme?

1 Like

I saw that when I was looking through your examples and very nearly added it. Doh!

Adding it has fixed my issue.

Is it possible to add something like this as a single class?

style:
  .: |
    ha-card {
      border: solid 1px var(--border-color);
      background: url("/local/background/card_bg_Night.png");
    }
    ha-card div.card-header {
      padding-top: 8px;
      padding-bottom: 36px;
    }

Sure. It should work if you use

ha-card.myclass {

and

ha-card.myclass div.card-header {
1 Like

Yes that works, except for one thing. When switching theme from:

  card-mod-theme: night
  card-mod-card: |
    ha-card.top-level-card {
      border: solid 1px var(--secondary-text-color);
      background: url("/local/background/card_bg_Night.png");
    }

    ha-card.top-level-card div.card-header {
      padding-top: 8px;
      padding-bottom: 36px;
    }

to

  card-mod-theme: day
  card-mod-card: |
    ha-card.top-level-card {
      border: solid 1px var(--primary-text-color);
      background: url("/local/background/card_bg_Day.png");
    }
    ha-card.top-level-card div.card-header {
      padding-top: 8px;
      padding-bottom: 36px;
    }

The card background does not update unless I navigate to another view and back, or refresh the page.

Possibly related console errors:

card-mod.js:1:2954
Uncaught (in promise) TypeError: null has no properties
    c https://redacted.duckdns.org/hacsfiles/lovelace-card-mod/card-mod.js:1

Leads here:

!function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)o.d(n,r,function(t){return e[t]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=1)}([function(e){e.exports=JSON.parse('{"name":"card-mod","private":true,"version":"2.0.0","description":"","scripts":{"build":"webpack","watch":"webpack --watch --mode=development","update-card-tools":"npm uninstall card-tools && npm install thomasloven/lovelace-card-tools"},"keywords":[],"author":"Thomas Lovén","license":"MIT","devDependencies":{"webpack":"^4.43.0","webpack-cli":"^3.3.11"},"dependencies":{"card-tools":"github:thomasloven/lovelace-card-tools"}}')},function(e,t,o){"use strict";o.r(t);const n=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),r=n.prototype.html;n.prototype.css;function a(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}let s=function(){if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage["lovelace-player-device-id"]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage["lovelace-player-device-id"]=`${e()}${e()}-${e()}${e()}`}return localStorage["lovelace-player-device-id"]}();const i=async e=>(await(async()=>{if(customElements.get("developer-tools-event"))return;await customElements.whenDefined("partial-panel-resolver");const e=document.createElement("partial-panel-resolver");e.hass={panels:[{url_path:"tmp",component_name:"developer-tools"}]},e._updateRoutes(),await e.routerOptions.routes.tmp.load(),await customElements.whenDefined("developer-tools-router");const t=document.createElement("developer-tools-router");await t.routerOptions.routes.event.load()})(),document.createElement("developer-tools-event")._computeParsedEventData(e)),l={template:"",variables:{},entity_ids:[]},c=async(e,t,o,n,r,a=!0)=>{e.localName.includes("-")&&await customElements.whenDefined(e.localName),e.updateComplete&&await e.updateComplete,e._cardMod=e._cardMod||document.createElement("card-mod"),(a?e.shadowRoot:e).appendChild(e._cardMod),await e.updateComplete,e._cardMod.type=t,e._cardMod.template={template:o,variables:n,entity_ids:r}};class d extends n{static get properties(){return{_renderedStyles:{},_renderer:{}}}static get applyToElement(){return c}constructor(){super(),document.querySelector("home-assistant").addEventListener("settheme",()=>{this._setTemplate(this._data)})}connectedCallback(){super.connectedCallback(),this.template=this._data,this.setAttribute("slot","none")}async getTheme(){if(!this.type)return null;let e=this.parentElement?this.parentElement:this;const t=window.getComputedStyle(e).getPropertyValue("--card-mod-theme"),o=a().themes.themes;return o[t]?o[t][`card-mod-${this.type}-yaml`]?await i(o[t][`card-mod-${this.type}-yaml`]):o[t]["card-mod-"+this.type]?o[t]["card-mod-"+this.type]:null:null}set template(e){e&&(this._data=JSON.parse(JSON.stringify(e)),this._setTemplate(this._data))}async _setTemplate(e){this._parent||(e.theme_template=await this.getTheme(),"string"==typeof e.template&&(e.template={".":e.template}),"string"==typeof e.theme_template&&(e.theme_template={".":e.theme_template})),e.template&&JSON.stringify(e.template).includes("config.entity")&&!e.entity_ids&&e.variables.config&&e.variables.config.entity&&(e.entity_ids=[e.variables.config.entity]),await this.setStyle(e)}async unStyle(){this._styledChildren=this._styledChildren||new Set;for(const e of this._styledChildren)e.template=l}_mergeDeep(e,t){const o=e=>e&&"object"==typeof e&&!Array.isArray(e);if(o(e)&&o(t))for(const n in t)o(t[n])?(e[n]||Object.assign(e,{[n]:{}}),"string"==typeof e[n]&&(e[n]={".":e[n]}),this._mergeDeep(e[n],t[n])):e[n]?e[n]=t[n]+e[n]:e[n]=t[n];return e}async setStyle(e){let{template:t,theme_template:o,variables:n,entity_ids:r}=e;if(await this.unStyle(),t||(t={}),t=JSON.parse(JSON.stringify(t)),this._mergeDeep(t,o),"string"==typeof t){if(this._renderedStyles=t,this._renderer){try{await this._renderer()}catch(e){if(!e.code||"not_found"!==e.code)throw e}this._renderer=void 0}return i=t,void((String(i).includes("{%")||String(i).includes("{{"))&&(this._renderer=await function(e,t,o){e||(e=a().connection);let n={user:a().user.name,browser:s,hash:location.hash.substr(1)||" ",...o.variables},r=o.template,i=o.entity_ids;return e.subscribeMessage(e=>{let o=e.result;o=o.replace(/_\([^)]*\)/g,e=>a().localize(e.substring(2,e.length-1))||e),t(o)},{type:"render_template",template:r,variables:n,entity_ids:i})}(null,e=>{this._renderedStyles=e},{template:t,variables:n,entity_ids:r})))}var i;await this.updateComplete;const l=this.parentElement||this.parentNode;if(!l)return{template:"",variable:variable,entity_ids:r};l.updateComplete&&await l.updateComplete;for(const e of Object.keys(t)){let o=[];if("."!==e){if("$"===e?(l.localName,o=[l.shadowRoot]):o=l.querySelectorAll(e),o.length)for(const a of o){if(!a)continue;let o=a.querySelector(":scope > card-mod");o&&o._parent===this||(o=document.createElement("card-mod"),this._styledChildren.add(o),o._parent=this),o.template={template:t[e],variables:n,entity_ids:r},a.appendChild(o)}}else this.setStyle({template:t[e],variables:n,entity_ids:r})}}createRenderRoot(){return this}render(){return r`
      <style>
        ${this._renderedStyles}
      </style>
    `}}if(!customElements.get("card-mod")){customElements.define("card-mod",d);const e=o(0);console.info(`%cCARD-MOD ${e.version} IS INSTALLED`,"color: green; font-weight: bold","")}function u(e,t,o=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},o)o.dispatchEvent(e);else{var n=function(){var e=document.querySelector("hc-main");return e=e?(e=(e=(e=e&&e.shadowRoot)&&e.querySelector("hc-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-view")||e.querySelector("hui-panel-view"):(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=document.querySelector("home-assistant"))&&e.shadowRoot)&&e.querySelector("home-assistant-main"))&&e.shadowRoot)&&e.querySelector("app-drawer-layout partial-panel-resolver"))&&e.shadowRoot||e)&&e.querySelector("ha-panel-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-root"))&&e.shadowRoot)&&e.querySelector("ha-app-layout"))&&e.querySelector("#view"))&&e.firstElementChild}();n&&n.dispatchEvent(e)}}customElements.whenDefined("ha-card").then(()=>{const e=customElements.get("ha-card");if(e.prototype.cardmod_patched)return;e.prototype.cardmod_patched=!0;const t=function(e){return e.config?e.config:e._config?e._config:e.host?t(e.host):e.parentElement?t(e.parentElement):e.parentNode?t(e.parentNode):null};e.prototype.firstUpdated=function(){const e=this.shadowRoot.querySelector(".card-header");e&&this.insertBefore(e,this.children[0]);const o=t(this);if(!o)return;o.class&&this.classList.add(o.class),o.type&&this.classList.add("type-"+o.type.replace(":","-"));(()=>{c(this,"card",o.style,{config:o},o.entity_ids,!1)})()},u("ll-rebuild",{})}),customElements.whenDefined("hui-entities-card").then(()=>{const e=customElements.get("hui-entities-card");if(e.prototype.cardmod_patched)return;e.prototype.cardmod_patched=!0;const t=e.prototype.renderEntity;e.prototype.renderEntity=function(e){const o=t.bind(this)(e);if(!e)return o;if(!o||!o.values)return o;const n=o.values[0];if(!n)return o;e.entity_ids;const r=()=>c(n,"row",e.style,{config:e},e.entity_ids);return r(),o.values[0]&&o.values[0].addEventListener("ll-rebuild",r),o},u("ll-rebuild",{})}),customElements.whenDefined("hui-glance-card").then(()=>{const e=customElements.get("hui-glance-card");e.prototype.cardmod_patched||(e.prototype.cardmod_patched=!0,e.prototype.firstUpdated=function(){this.shadowRoot.querySelectorAll("ha-card div.entity").forEach(e=>{const t=e.attachShadow({mode:"open"});[...e.children].forEach(e=>t.appendChild(e));const o=document.createElement("style");t.appendChild(o),o.innerHTML="\n      :host {\n        box-sizing: border-box;\n        padding: 0 4px;\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        cursor: pointer;\n        margin-bottom: 12px;\n        width: var(--glance-column-width, 20%);\n      }\n      div {\n        width: 100%;\n        text-align: center;\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n      .name {\n        min-height: var(--paper-font-body1_-_line-height, 20px);\n      }\n      state-badge {\n        margin: 8px 0;\n      }\n      ";const n=e.config||e.entityConf;if(!n)return;n.entity_ids;c(e,"glance",n.style,{config:n},n.entity_ids)})},u("ll-rebuild",{}))}),customElements.whenDefined("hui-state-label-badge").then(()=>{const e=customElements.get("hui-state-label-badge");e.prototype.cardmod_patched||(e.prototype.cardmod_patched=!0,e.prototype.firstUpdated=function(){const e=this._config;if(!e)return;e.entity_ids;(()=>{c(this,"badge",e.style,{config:e},e.entity_ids)})()},u("ll-rebuild",{}))}),customElements.whenDefined("hui-view").then(()=>{const e=customElements.get("hui-view");e.prototype.cardmod_patched||(e.prototype.cardmod_patched=!0,e.prototype.firstUpdated=function(){(()=>{c(this,"view","",{},[])})()},u("ll-rebuild",{}))}),customElements.whenDefined("hui-root").then(()=>{const e=customElements.get("hui-root");if(e.prototype.cardmod_patched)return;e.prototype.cardmod_patched=!0,e.prototype.firstUpdated=async function(){(()=>{c(this,"root","",{},[])})()},u("ll-rebuild",{});let t=document.querySelector("home-assistant");t=t&&t.shadowRoot,t=t&&t.querySelector("home-assistant-main"),t=t&&t.shadowRoot,t=t&&t.querySelector("app-drawer-layout partial-panel-resolver"),t=t&&t.querySelector("ha-panel-lovelace"),t=t&&t.shadowRoot,t=t&&t.querySelector("hui-root"),t&&t.firstUpdated()}),customElements.whenDefined("ha-more-info-dialog").then(()=>{const e=customElements.get("ha-more-info-dialog");if(e.prototype.cardmod_patched)return;e.prototype.cardmod_patched=!0;const t=e.prototype.showDialog;e.prototype.showDialog=function(e){const o=()=>{c(this.shadowRoot.querySelector("ha-dialog"),"more-info","",{config:e},[e.entityId],!1)};t.bind(this)(e),this.requestUpdate().then(async()=>{await this.shadowRoot.querySelector("ha-dialog").updateComplete,o()})};let o=document.querySelector("home-assistant");o=o&&o.shadowRoot,o=o&&o.querySelector("ha-more-info-dialog"),o&&(o.showDialog=e.prototype.showDialog.bind(o),o.showDialog({entityId:o.entityId}))});let p=window.cardHelpers;const h=new Promise(async(e,t)=>{p&&e();const o=async()=>{p=await window.loadCardHelpers(),window.cardHelpers=p,e()};window.loadCardHelpers?o():window.addEventListener("load",async()=>{!function(){if(customElements.get("hui-view"))return!0;const e=document.createElement("partial-panel-resolver");if(e.hass=a(),!e.hass||!e.hass.panels)return!1;e.route={path:"/lovelace/"},e._updateRoutes();try{document.querySelector("home-assistant").appendChild(e)}catch(e){}finally{document.querySelector("home-assistant").removeChild(e)}customElements.get("hui-view")}(),window.loadCardHelpers&&o()})});function m(e,t){const o={type:"error",error:e,origConfig:t},n=document.createElement("hui-error-card");return customElements.whenDefined("hui-error-card").then(()=>{const e=document.createElement("hui-error-card");e.setConfig(o),n.parentElement&&n.parentElement.replaceChild(e,n)}),h.then(()=>{u("ll-rebuild",{},n)}),n}function y(e,t){if(!t||"object"!=typeof t||!t.type)return m(`No ${e} type configured`,t);let o=t.type;if(o=o.startsWith("custom:")?o.substr("custom:".length):`hui-${o}-${e}`,customElements.get(o))return function(e,t){let o=document.createElement(e);try{o.setConfig(JSON.parse(JSON.stringify(t)))}catch(e){o=m(e,t)}return h.then(()=>{u("ll-rebuild",{},o)}),o}(o,t);const n=m(`Custom element doesn't exist: ${o}.`,t);n.style.display="None";const r=setTimeout(()=>{n.style.display=""},2e3);return customElements.whenDefined(o).then(()=>{clearTimeout(r),u("ll-rebuild",{},n)}),n}const f="\nha-card {\n  background: none;\n  box-shadow: none;\n}";customElements.define("mod-card",class extends n{static get properties(){return{hass:{}}}setConfig(e){this._config=JSON.parse(JSON.stringify(e)),void 0===e.style?this._config.style=f:"string"==typeof e.style?this._config.style=f+e.style:e.style["."]?this._config.style["."]=f+e.style["."]:this._config.style["."]=f,this.card=function(e){return p?p.createCardElement(e):y("card",e)}(this._config.card),this.card.hass=a()}render(){return r`
          <ha-card modcard>
          ${this.card}
          </ha-card>
        `}set hass(e){this.card&&(this.card.hass=e)}getCardSize(){if(this._config.report_size)return this._config.report_size;let e=this.shadowRoot;return e&&(e=e.querySelector("ha-card card-maker")),e&&(e=e.getCardSize),e&&(e=e()),e||1}})}]);

It is very fast though and I like that I am no longer messing with system cards like the configuration / integration page (the background image did not fit well).

Opened an issue:

One final thing and my supercharged theme will be complete.

I can not get this style:

    style:
      .: |
        ha-card {
          border: solid 1px var(--primary-color);
        }
      mmp-shortcuts:
        $: |
          mmp-button {
            box-shadow: none;
            background: none;
            border: solid 1px var(--primary-color);
            border-radius: 10px;
          } 

To work as class: media-player . This is what I have:

    ha-card.media-player {
      border: solid 1px var(--secondary-text-color);
    }

    mmp-shortcuts.media-player {
      $: |
        mmp-button {
          box-shadow: none;
          background: none;
          border: solid 1px var(--secondary-text-color);
          border-radius: 10px;
        }
    }

But it only shows the border. The button stying does not occur. I have tried with and without the pipe symbol after the shadow root.

The ha-card element is the one that gets the class added, and the mmp-shortcuts element is a child of that, so ha-card.media-player mmp-shortcuts { may work.

I have a weird issue. I finally decided to bite the bullet to 113 and updated many card-mod and browser-mod related stuff. All works great now (a few minor glitches aside). One thing I do notice though: the more-info styling doesn’t work properly on iOS. I used to have a backdrop filter with a blurred effect. That pluging doesn’t work anymore, but card-mod 2.0 supports that as well. I use the exact same theme across all my devices (iOS, Android and Chrome browser on Win10) and the following style (under theme yaml) works excellent on desktop/Android:

  card-mod-theme: hohm-one-black
  card-mod-more-info-yaml: |
    $: |
      .mdc-dialog {
        backdrop-filter: blur(17px);
        background: rgba(0,0,0,0.5);
      }
      .mdc-dialog .mdc-dialog__container .mdc-dialog__surface {
        background: none !important;
        box-shadow: none;
        border-radius: 20px;
      }
    ha-header-bar:
      $: |
        .mdc-top-app-bar {
          background: none !important;
        }   

It gives me a nice blurred darkened background. On iOS however, it does not apply the blur. It does apply the darkened effect. Is this an iOS related issue? I haven’t seen a post yet, so that’s why I wanted to ask around. I cleared cache withing HA app (also withing iOS settings for Safari) and tried with the HA app and Safari on iOS.

I also notice the header bar is white on iOS, while it is transparent as it should be on desktop. It seems to take the primary-color (or background-color, those are same color on my theme). When I use my night theme, the status bar is black on iOS (while transparent on desktop).

This is on Edge (Chromium) on Win10.

This is on iOS

To be fully informative, I use light-popup-slider card with the following styling:

      $: |
        .mdc-dialog .mdc-dialog__container {
          width: 100%;
        }
        .mdc-dialog .mdc-dialog__container .mdc-dialog__surface {
          width:100%;
          box-shadow:none;
        }
      .: |
        :host {
          --mdc-theme-surface: rgba(0,0,0,0);
          --secondary-background-color: rgba(0,0,0,0);
          # --ha-card-background: rgba(0,0,0,8);
          # --mdc-dialog-scrim-color: rgba(0,0,0,0.8);
          --mdc-dialog-min-height: 100%;
          --mdc-dialog-min-width: 100%;
          --mdc-dialog-max-width: 100%;
        }
        mwc-icon-button {
          color: var(--text-color);
        }

Perhaps these stylings are working against each other?

Any help if you have the time is greatly appreciated.

Can you please tell me about the “class” section you have under the Lovelace card setup? I didn’t see any of this in the example theme, unless I’m missing a file. Does that need to be on every card? Can you define multiple themes for the same Lovelace card type? Inheritance? Overrides?

Thomas did a quick explanation here:

HI Thomas, might I ask in this thread about an issue I have since updating (either to HA 113, or card-mod, I cant say honestly) and point here to prevent crossposting too much?

Having an inspector error, while HA seems to do alright in the frontend.
Hope you ca have a look, thanks.

Thanks Thomas, but unfortunately no, this does not replace the button style either.

  card-mod-theme: night
  card-mod-card: |

    ha-card.media-player {
      border: solid 1px var(--secondary-text-color);
    }

    ha-card.media-player mmp-shortcuts {
      $: |
        mmp-button {
          box-shadow: none;
          background: none;
          border: solid 1px var(--secondary-text-color);
          border-radius: 10px;
        }
    }

https://caniuse.com/#feat=css-backdrop-filter
Try with the -webkit- prefix.

2 Likes

That did the trick! I added that as an extra line so I have it on both iOS devices and Android/Chromium.

I think the status bar not becoming transparant on iOS has something to with the popup-light-card I’m using. Standard ‘more info’ views are transparent. Gonna dig deeper into that.

Thanks a lot for your time and help, once again :slight_smile:

edit:

I did some more digging around, and found out that the popup bar changes to background-color depending on the size. When I resize it untill it goes white on desktop, I see the following line:

@media (max-width: 450px), (max-height: 500px)
app-toolbar {
    background-color: #FFFFFF;
    color: #404040;
}

The weird thing is, I don’t know why this happens with popup cards only. More info dialogs don’t have that but opening any other popup card does.