A powerful Home Assistant custom card that dynamically loops over template-generated lists to create multiple cards without manual configuration.
Perfect for displaying dynamic content like:
- π Todoist tasks and reminders
- π Calendar events
- π‘οΈ Sensor arrays (temperatures, batteries, etc.)
- π₯ Person locations
- π Shopping lists
- π Any list-based data from Home Assistant
- π Dynamic Looping: Process Jinja2 templates to generate lists of cards
- π― Universal Card Support: Works with all built-in Home Assistant card types plus custom cards
- π Flexible Layouts: Vertical, horizontal, and grid layouts with configurable spacing
- π‘οΈ Error Handling: Graceful error recovery with configurable fallback messages
- π¨ Styling Support: Full card-mod integration for custom styling
- β‘ Performance: Efficient rendering and updates
- π§ Easy Configuration: Intuitive YAML configuration
- Open HACS in your Home Assistant instance
- Go to "Frontend" section
- Click the "+" button and search for "Template Loop Card"
- Install the card
- Restart Home Assistant
- Add the card to your dashboard
- Download
template-loop-card.js
from the latest release - Copy it to your
config/www/
folder - Add the resource to your Lovelace configuration:
resources:
- url: /local/template-loop-card.js
type: module
- Restart Home Assistant
type: custom:template-loop-card
template: >
{% for item in state_attr('sensor.todoist_reminders', 'items') %}
- type: markdown
content: "**{{ item.content }}** - due: {{ item.due.date }}"
{% endfor %}
Option | Type | Default | Description |
---|---|---|---|
template |
string | Required | Jinja2 template that returns a list of card configurations |
layout |
string | vertical |
Layout type: vertical , horizontal , or grid |
columns |
number | 2 |
Number of columns for grid layout |
spacing |
string | 8px |
Gap between cards |
no_items_message |
string | No items found |
Message shown when template returns no items |
show_errors |
boolean | false |
Show detailed error messages in the UI |
card_mod |
object | - | Card-mod styling configuration |
type: custom:template-loop-card
template: >
{% for task in state_attr('sensor.todoist_tasks', 'items') %}
- type: markdown
content: |
**{{ task.content }}**
{% if task.due %}π
Due: {{ task.due.date }}{% endif %}
β Priority: {{ task.priority }}
{% endfor %}
layout: vertical
spacing: 12px
no_items_message: 'All tasks completed! π'
type: custom:template-loop-card
template: >
{% for event in state_attr('calendar.personal', 'events')[:5] %}
- type: entities
title: "π
{{ event.summary }}"
entities:
- entity: input_datetime.dummy
name: "π {{ event.start.strftime('%H:%M') }}"
- entity: input_text.dummy
name: "π {{ event.location or 'No location' }}"
{% endfor %}
type: custom:template-loop-card
template: >
{% for sensor in states.sensor if 'temperature' in sensor.entity_id %}
- type: button
entity: "{{ sensor.entity_id }}"
name: "{{ sensor.attributes.friendly_name }}"
show_state: true
icon: mdi:thermometer
{% endfor %}
layout: grid
columns: 3
spacing: 8px
type: custom:template-loop-card
template: >
{% for person in states.person %}
- type: markdown
content: |
{% if person.state == 'home' %}
π **{{ person.attributes.friendly_name }}** is home
{% else %}
π **{{ person.attributes.friendly_name }}** is {{ person.state }}
{% endif %}
{% endfor %}
layout: horizontal
spacing: 16px
type: custom:template-loop-card
template: >
{% for item in state_attr('sensor.shopping_list', 'items') %}
- type: markdown
content: |
{% if item.complete %}
β
~~{{ item.name }}~~
{% else %}
β¬ {{ item.name }}
{% endif %}
{% endfor %}
no_items_message: 'Shopping list is empty! π'
type: custom:template-loop-card
template: >
{% for state in states if state.attributes.battery_level is defined and state.attributes.battery_level | int < 20 %}
- type: button
entity: "{{ state.entity_id }}"
name: "{{ state.attributes.friendly_name }}"
show_state: true
icon: mdi:battery-low
{% endfor %}
layout: grid
columns: 2
no_items_message: 'All batteries are good! π'
type: custom:template-loop-card
template: >
{% for light in states.light if light.state == 'on' %}
- type: button
entity: "{{ light.entity_id }}"
name: "{{ light.attributes.friendly_name }}"
icon: mdi:lightbulb
{% endfor %}
card_mod:
style: |
ha-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
border: none;
}
.template-loop-container {
padding: 20px;
}
.template-loop-item {
transform: scale(0.95);
transition: transform 0.2s ease;
}
.template-loop-item:hover {
transform: scale(1);
}
- Node.js 18+
- npm or yarn
# Clone the repository
git clone https://github.com/EnkodoNL/template-loop-card.git
cd template-loop-card
# Install dependencies
npm install
# Build the card
npm run build
# Watch for changes during development
npm run dev
template-loop-card/
βββ src/
β βββ template-loop-card.ts # Main card component
β βββ template-processor.ts # Template processing logic
β βββ card-renderer.ts # Card rendering engine
β βββ error-handler.ts # Error handling utilities
β βββ types.ts # TypeScript type definitions
βββ dist/ # Built files
βββ hacs.json # HACS configuration
βββ info.md # HACS store information
βββ README.md # This file
Problem: Template not working or showing errors
Solutions:
- Enable
show_errors: true
to see detailed error messages - Test your template in Home Assistant's Developer Tools > Template
- Check the browser console for additional debugging information
- Ensure your template returns a valid list of card configurations
Problem: Card doesn't show up in the dashboard
Solutions:
- Verify the resource is added to your Lovelace configuration
- Clear browser cache (Ctrl+F5 or Cmd+Shift+R)
- Check that the card type is exactly
custom:template-loop-card
- Restart Home Assistant after installation
Problem: Dashboard becomes slow with large lists
Solutions:
- Limit items in your template using slicing:
states.sensor[:10]
- Use simpler card types (markdown instead of entities)
- Avoid complex calculations in templates
- Consider pagination for very large datasets
# Limit number of items
{% for item in items[:5] %}
# Filter by condition
{% for sensor in states.sensor if sensor.state != 'unavailable' %}
# Sort by attribute
{% for item in items | sort(attribute='name') %}
# Group by category
{% for category, items in items | groupby('category') %}
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- π Report Issues
- π¬ Community Forum
- π Documentation
β If you find this card useful, please consider giving it a star on GitHub!