TailwindCSS Template Card - build custom cards with HTML and TailwindCSS

I hope this feedback will give you some guidance. I really love this approach and I strongly believe this project has huge potential for nice dashboards.

Currently I’m seeing a lot of latency and I didn’t build many components in the same view yet. Benchmarking is done comparing tailwind elements in my view with native elements from home assistant in the same view.

  1. Very often tailwind components are not loaded at all or very slow. Native components show instantly.
  2. Changing entity status is not immediately visible in the tailwind elements (using the predefined classes to reflect a change). Ex: green color of a button means status is on and no color is reflecting status off.
  3. Changing status of a device (only tested light) is picked up instantly using the onclick event with the hass services.

Would the impact of running this setup locally (not loading the entire framework over and over again from the cloud) have a positive result?
Can we cache the tailwind framework once loaded to speed up loading times?

Thanks for the feedback! Which version are you using! The latest v2.0.3 brings some interesting performance improvements

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?