TailwindCSS Template Card - build custom cards with HTML and TailwindCSS

Updated to v2.0.3 and will retest. Feedback will follow soon.

Is the onchange event on an input element that is used with a drag not going to fire every change it sees? In other words, if I drag the slider from 0 to 255, is this going to hit the event 255 times and thus call the Hass service 255 times? Can we use something like focused?

Change event fires only when value has been committed (see here: HTMLElement: change event - Web APIs | MDN). In the case of a range input, only when you release.

@usernein This is awesome. This would make styling custom cards a breeze. I’ll give it a try in the next few days and report back.

One immediate concern or comment I have is embedding logic (JavaScript) inside the content itself. It would be nice if there was a way to have the chance to specify a script that could handle all the interaction with the HTML content you’re rendering.

I found this (New lovelace custom card approach - fully customizable card with your own design) a few minutes ago, where there’s a clear separation between content, binding and behavior. I don’t necessarily think binding is needed, but scripts (behavior) definitely is when you start working on more complex interactive cards.

I know this might go beyond the “style cards using Tailwind” approach shown here, but I’d love to see a solution to create custom cards in a more lightweight manner instead of creating a full-blown custom component.

Let me know what you think.

1 Like

Wow, this is absolutely awesome.

Is there any way to use the Home Assistant SVG icons (MDI for example)?

For example, you can just use this inside content:

<ha-icon icon="mdi:ceiling-fan-light"></ha-icon>

1 Like

@silvio we are on the same page! I
That’s exactly what i did on the first example: call a service on home assistant passing the value of the input. You just need to adjust it to use the home assistant service that you want.

But i did the work for you (and tested):

<div class="card w-full bg-base-100 shadow-xl">
  <figure><img src="https://picsum.photos/300/100" alt="Keuken" /></figure>
  <div class="card-body">
    <h2 class="card-title">
      Banheiro
      <div class="badge badge-secondary"> {{ 'fout' if is_state('light.banheiro', 'unavailable') else ''}} </div>
    </h2>
    <p>This is a light card wip</p>

    <div class="card-actions"> 
<button class="btn btn-wide {{ 'btn-success' if is_state('light.banheiro', 'on') else ''}}" onClick="hass.callService('homeassistant', 'toggle', {entity_id: 'light.banheiro'})">{{ 'aan' if is_state('light.banheiro', 'on') else 'uit'}}</button>
<div class="flex">
  <div class="flex-auto w-64">
    <input type="range" min="0" max="255" value={{ state_attr('light.banheiro', 'brightness') }} class="range range-lg pr-10" onChange="hass.callService('light', 'turn_on', {'entity_id': 'light.banheiro', brightness: this.value})"/>
  </div>
  <div class="flex-auto w-32">
    <input type="checkbox" class="toggle toggle-success toggle-lg" {{ 'checked' if is_state('light.banheiro', 'on') else 'unchecked'}} onClick="hass.callService('homeassistant', 'toggle', {entity_id: 'light.banheiro'})" />
  </div>
  
</div>


</div>
<div class="card-actions justify-end"> 
      <div class="badge badge-outline">{{expand('light.banheiro')|selectattr('state','eq','on')|list|count}} / {{expand('light.lichten_keuken')|list|count}}</div> 
      <div class="badge badge-outline">
{{states('light.banheiro')}} 
{% if(is_number(state_attr('light.banheiro', 'brightness')))%}
{% set bri= state_attr('light.banheiro', 'brightness')%}
{% set bri = (bri/2.55)|round(0)%}
{%else%}
{% set bri=0%}
{%endif%}
{{bri}}
%</div>
    </div>
  </div>
</div>

Note that i changed the input range to 0-255

@silvio About onchange, it will run only on release, as @fabianluque said. If you want to update instantly, use “onInput”, but be aware that if you use jinja templates to fetch entities values, the card will update everytime the entities update

@fabianluque i took a while to fully understand how amazing the approach of the project you shared is

I totally agree with you. I was already dissapointed on how long complex cards as the last examples i shared here can be. Specially when you need to write the same entity_id again and again or when you need to have the card synced with the state.
I was even thinking about using aliases for “hass.callService”

I got excited by the idea, so i can confirm that you will see this amazing feature very soon.

1 Like

What I do to solve the issue of having to rewrite the hass element over and over again is the following. I define my light and other checks as illustrated below:

{{% set lichten = 'light.lichten_keuken' %}
{% set attr_bri = 'brightness' in state_attr(lichten, 'supported_color_modes') %}

<div class="card w-full bg-base-100 shadow-xl">
  <div class="bg-cover bg-center h-56 p-4" style="background-image: url(/local/images/ui_elements/lamp-kort.png)">
                        <div class="flex justify-end">
<ha-icon icon="mdi:ceiling-fan-light"></ha-icon>
                            <svg class="h-6 w-6 text-black fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M12.76 3.76a6 6 0 0 1 8.48 8.48l-8.53 8.54a1 1 0 0 1-1.42 0l-8.53-8.54a6 6 0 0 1 8.48-8.48l.76.75.76-.75zm7.07 7.07a4 4 0 1 0-5.66-5.66l-1.46 1.47a1 1 0 0 1-1.42 0L9.83 5.17a4 4 0 1 0-5.66 5.66L12 18.66l7.83-7.83z"></path>
                            </svg>
<div class="relative">
    <div class="w-16 h-16 bg-white rounded-lg shadow-2xl"></div>
    <div class="absolute top-0 right-0 -mr-1 -mt-1 w-4 h-4 rounded-full bg-green-300 animate-ping"></div>
    <div class="absolute top-0 right-0 -mr-1 -mt-1 w-4 h-4 rounded-full bg-green-300"></div>
  </div>
                        </div>
                    </div>
  <div class="card-body">
    <h2 class="card-title">
      KEUKEN
      <div class="badge {{ 'badge-secondary' if is_state(lichten, 'unavailable') else 'badge-success'}}"> {{ 'fout' if is_state(lichten, 'unavailable') else 'ok'}} </div>
    </h2>
    <p> {% if (expand(lichten)|selectattr('state','in',['unavailable','unknown','none'])|list|count) ==0 %}
Alle lampen zijn ok.
        {% else%}
{{ expand(lichten)|selectattr('state','in',['unavailable','unknown','none'])|list|map(attribute='name')|join('\n- ')}}
        {%endif%}
</p>

    <div class="card-actions"> 
<button class="btn btn-wide {{ 'btn-success' if is_state(lichten, 'on') else ''}}" onClick="hass.callService('homeassistant', 'toggle', {entity_id: lichten})">{{ 'aan' if is_state(lichten, 'on') else 'uit'}}</button>
<div class="flex">
  <div class="flex-auto w-64  {% if (attr_bri==false)%}hidden{%endif%}">
<input type="range" min="0" max="255" value="{% if(is_number(state_attr(lichten, 'brightness')))%}{% set bri= state_attr(lichten, 'brightness')|round(0)%}{%else%}{% set bri=0%}{%endif%}{{bri|round(0)}}" class="range range-lg pr-10" onchange="hass.callService('light', 'turn_on', { entity_id: lichten, brightness: this.value })" class="range" step="25" />

  </div>
  <div class="flex-auto w-32">
    <input type="checkbox" class="toggle toggle-success toggle-lg" {{ 'checked' if is_state('automation.keuken_pir_beweging', 'on') else 'unchecked'}} onClick="hass.callService('homeassistant', 'toggle', {entity_id: 'automation.keuken_pir_beweging'})" />
  </div>
  
</div>


</div>
<div class="card-actions justify-end"> 
      <div class="badge badge-outline">{{expand(lichten)|selectattr('state','eq','on')|list|count}} / {{expand(lichten)|list|count}}
</div> 
      <div class="badge badge-outline">
{{states(lichten)}} 
{% if(is_number(state_attr(lichten, 'brightness')))%}
{% set bri= state_attr(lichten, 'brightness')%}
{% set bri = (bri/2.55)|round(0)%}
{%else%}
{% set bri=0%}
{%endif%}
{{bri}}
%</div>
    </div>
  </div>
<div class="collapse bg-base-200">
  <input type="checkbox" /> 
  <div class="collapse-title text-xl font-medium">
    Click me to show/hide content
  </div>
  <div class="collapse-content"> 
    <p>hello</p>
  </div>
</div>
</div>

Next nice feature would be to be able to use the hass icons locally. As they already exist in the system, we don’t want to pull them over and over from the cloud. Can this be done?

@silvio About the icons: TailwindCSS Template Card - build custom cards with HTML and TailwindCSS - #27 by fabianluque

Tx for the help guys.

Could you have a look at this? Maybe you can determine why it’s not running? It doesn’t recognize the variables.

{% set lichten = 'light.lichten_keuken' %}
{% set attr_bri = 'brightness' in state_attr(lichten, 'supported_color_modes') %}

<div class="card w-full bg-base-100 shadow-xl">
  <div class="bg-cover bg-center h-56 p-4" style="background-image: url(/local/images/ui_elements/lamp-kort.png)">
                        <div class="flex justify-end">
<ha-icon icon="mdi:ceiling-fan-light"></ha-icon>
                            <svg class="h-6 w-6 text-black fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
                                <path d="M12.76 3.76a6 6 0 0 1 8.48 8.48l-8.53 8.54a1 1 0 0 1-1.42 0l-8.53-8.54a6 6 0 0 1 8.48-8.48l.76.75.76-.75zm7.07 7.07a4 4 0 1 0-5.66-5.66l-1.46 1.47a1 1 0 0 1-1.42 0L9.83 5.17a4 4 0 1 0-5.66 5.66L12 18.66l7.83-7.83z"></path>
                            </svg>
<div class="relative">
    <div class="w-16 h-16 bg-white rounded-lg shadow-2xl"></div>
    <div class="absolute top-0 right-0 -mr-1 -mt-1 w-4 h-4 rounded-full bg-green-300 animate-ping"></div>
    <div class="absolute top-0 right-0 -mr-1 -mt-1 w-4 h-4 rounded-full bg-green-300"></div>
  </div>
                        </div>
                    </div>
  <div class="card-body">
    <h2 class="card-title">
      KEUKEN
      <div class="badge {{ 'badge-secondary' if is_state(lichten, 'unavailable') else 'badge-success'}}"> {{ 'fout' if is_state(lichten, 'unavailable') else 'ok'}} </div>
    </h2>
    <p> {% if (expand(lichten)|selectattr('state','in',['unavailable','unknown','none'])|list|count) ==0 %}
Alle lampen zijn ok.
        {% else%}
{{ expand(lichten)|selectattr('state','in',['unavailable','unknown','none'])|list|map(attribute='name')|join('\n- ')}}
        {%endif%}
</p>

    <div class="card-actions"> 
<button class="btn btn-wide {{ 'btn-success' if is_state(lichten, 'on') else ''}}" onClick="hass.callService('homeassistant', 'toggle', {entity_id: lichten})">{{ 'aan' if is_state(lichten, 'on') else 'uit'}}</button>
<div class="flex">
  <div class="flex-auto w-64  {% if (attr_bri==false)%}hidden{%endif%}">
<input type="range" min="0" max="255" value="{% if(is_number(state_attr(lichten, 'brightness')))%}{% set bri= state_attr(lichten, 'brightness')|round(0)%}{%else%}{% set bri=0%}{%endif%}{{bri|round(0)}}" class="range range-lg pr-10" onchange="hass.callService('light', 'turn_on', { entity_id: lichten, brightness: this.value })" class="range" step="25" />

  </div>
  <div class="flex-auto w-32">
    <input type="checkbox" class="toggle toggle-success toggle-lg" {{ 'checked' if is_state('automation.keuken_pir_beweging', 'on') else 'unchecked'}} onClick="hass.callService('homeassistant', 'toggle', {entity_id: 'automation.keuken_pir_beweging'})" />
  </div>
  
</div>


</div>
<div class="card-actions justify-end"> 
      <div class="badge badge-outline">{{expand(lichten)|selectattr('state','eq','on')|list|count}} / {{expand(lichten)|list|count}}
</div> 
      <div class="badge badge-outline">
{{states(lichten)}} 
{% if(is_number(state_attr(lichten, 'brightness')))%}
{% set bri= state_attr(lichten, 'brightness')%}
{% set bri = (bri/2.55)|round(0)%}
{%else%}
{% set bri=0%}
{%endif%}
{{bri}}
%</div>
    </div>
  </div>
<div class="collapse bg-base-200">
  <input type="checkbox" /> 
  <div class="collapse-title text-xl font-medium">
    Click me to show/hide content
  </div>
  <div class="collapse-content"> 
    <p>hello</p>
  </div>
</div>
</div>

I found my mistake. Lichten cannot be called from onclick as it’s not a known var in th js scope. Rookie mistake.

Despite correcting the issue by calling the entity by the entity_id known in Hass, I still have many freezes. After loading the view with the tailwind UI the elements don’t appear. Very often it doesn’t load the objects at all, but the native Hass objects are loaded fine in the view.

When the tailwind components are loaded correctly in the view, the buttons to toggle my light (onclick) don’t respond at all. It’s strange because after reboot the onclick is fast as native (very rare), after a few clicks I have delays going up to 10 seconds but more often I have no response at all. This what my log shows:

Hey @fabianluque you can already test the Bindings on the beta version v2.1.0-1, which is already downloadable through HACS (as a beta version).

Although i wrote it from scratch in TypeScript, it works on the exact same way of the approach you shared

I’m not able to download using HACS, compared to other versions seems the .js file is missing in the assets of the release.

Same here.

My fault, sorry! Can you try again with v2.1.0-7?

Great, now it worked.

Did a quick test with a binding, works for first render but if state is updated it doesn’t refresh. Here’s a sample configuration:

      - type: custom:tailwindcss-template-card
        entity: light.attic_lights
        bindings:
          - bind: return entity.state
            selector: span
            type: text
        content: |
          <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
            Attic Lights State: <span></span>
          </button>

This is great progress! Thanks so much for going ahead and implementing this idea, really appreciated.

Spoke too soon, if I add the option below it updates just fine:

        always_update: true

Not sure if that’s what’s intended, but it makes it work.