HTML Templates
Note: PyView supports two templating approaches. See Templating Overview to compare options, or check out T-String Templates for the Python 3.14+ alternative.
PyView uses a modified version of the ibis template engine with additional LiveView-specific features. This guide covers template syntax, variables, control structures, filters, and the rendering system.
Template Basics
Template Location
PyView automatically finds templates by looking for .html files in the same directory as your LiveView class:
views/├── counter.py # CounterLiveView class├── counter.html # Template for CounterLiveView├── counter.css # Optional - automatically included├── user_list.py # UserListLiveView class└── user_list.html # Template for UserListLiveViewAutomatic CSS Inclusion
If you create a .css file with the same name as your LiveView, PyView automatically includes it in the page:
views/├── counter.py├── counter.html└── counter.css # Automatically wrapped in <style> tagsThis makes it easy to keep component-specific styles alongside your templates without manually managing style imports.
Basic Template Structure
Templates combine HTML with template syntax for dynamic content:
<div class="counter"> <h1>Count: {{count}}</h1> <button phx-click="increment">+</button> <button phx-click="decrement">-</button></div>The corresponding LiveView provides the count variable:
class CounterLiveView(LiveView[CountContext]): async def mount(self, socket: LiveViewSocket[CountContext], session): socket.context = {"count": 0} # Available as {{count}} in templateTemplate Syntax
Variables
Display values using double curly braces:
<!-- Basic variables --><h1>{{title}}</h1><p>Welcome, {{user.name}}!</p><span>{{item.price}}</span>
<!-- Nested access --><div>{{user.profile.bio}}</div><img src="{{user.avatar.url}}" alt="{{user.name}}'s avatar">Comments
Template comments are not rendered in the output:
{# This is a comment - won't appear in HTML #}<div>{{content}}</div>{# TODO: Add pagination controls #}Control Structures
See the ibis tag reference for complete documentation.
Conditional Rendering (if)
Show/hide content based on conditions:
<!-- Basic if -->{% if user.is_admin %} <button>Admin Panel</button>{% endif %}
<!-- if/else -->{% if items %} <ul> {% for item in items %} <li>{{item.name}}</li> {% endfor %} </ul>{% else %} <p>No items found.</p>{% endif %}
<!-- Multiple conditions -->{% if user.is_authenticated %} {% if user.has_permission %} <button>Edit</button> {% else %} <span>View Only</span> {% endif %}{% else %} <a href="/login">Login</a>{% endif %}Loops (for)
Iterate over lists and collections:
<!-- Basic loop --><ul> {% for user in users %} <li>{{user.name}} - {{user.email}}</li> {% endfor %}</ul>
<!-- Loop with conditionals --><div class="user-list"> {% for user in users %} <div class="user-card {% if user.is_active %}active{% else %}inactive{% endif %}"> <h3>{{user.name}}</h3> <p>{{user.email}}</p> {% if user.is_admin %} <span class="badge">Admin</span> {% endif %} </div> {% endfor %}</div>
<!-- Empty list handling -->{% for message in messages %} <div class="message">{{message.text}}</div>{% empty %} <p>No messages yet.</p>{% endfor %}Loop Variables
Access loop metadata with the built-in loop variable:
<table> {% for item in items %} <tr> <td>{{loop.count}}</td> <td>{{item.name}}</td> <td> {% if loop.is_first %}First{% endif %} {% if loop.is_last %}Last{% endif %} </td> </tr> {% endfor %}</table>Available loop variables:
loop.index- Current iteration (0-indexed)loop.count- Current iteration (1-indexed)loop.length- Total number of items in the sequenceloop.is_first- True if first iterationloop.is_last- True if last iterationloop.parent- For nested loops, the loop variable of the parent loop
Filters
See the ibis filter reference for complete documentation.
Transform values using the pipe operator:
Built-in Filters
<!-- Text formatting --><h1>{{title | upper}}</h1><p>{{description | lower}}</p><span>{{name | title}}</span>
<!-- Date/time formatting --><time>{{created_at | dtformat:"%Y-%m-%d %H:%M"}}</time><span>{{updated | dtformat:"%b %d, %Y"}}</span>
<!-- Lists and sequences --><p>{{tags | join:", "}}</p><span>{{users | len}} users</span><div>{{items | first}}</div><div>{{items | last}}</div>
<!-- HTML and safety --><div>{{content | escape}}</div><div>{{html_content | striptags}}</div>
<!-- Default values --><span>{{optional_field | default:"Not provided"}}</span><img src="{{avatar | default:"/static/default-avatar.png"}}">Common Filters Reference
| Filter | Purpose | Example |
|---|---|---|
upper | Uppercase text | {{name | upper}} |
lower | Lowercase text | {{name | lower}} |
title | Title case | {{name | title}} |
len | Get length | {{items | len}} |
join | Join with separator | {{tags | join:", "}} |
default | Default value | {{field | default:"None"}} |
escape | HTML escape | {{content | escape}} |
dtformat | Date formatting | {{date | dtformat:"%Y-%m-%d"}} |
truncatechars | Truncate characters | {{text | truncatechars:50}} |
truncatewords | Truncate words | {{text | truncatewords:10}} |
first | First item | {{items | first}} |
last | Last item | {{items | last}} |
Custom Filters
Register your own filters using the @register decorator:
from pyview.vendor.ibis.filters import register
@registerdef currency(value, symbol="$"): """Format a number as currency.""" return f"{symbol}{value:,.2f}"
@register("pluralize")def pluralize_filter(count, suffix="s"): """Add suffix if count != 1.""" return "" if count == 1 else suffixUse in templates:
<span>{{price | currency}}</span> <!-- $1,234.56 --><span>{{price | currency:"€"}}</span> <!-- €1,234.56 --><span>{{count}} item{{count | pluralize}}</span> <!-- 5 items -->Template Includes
See ibis template inheritance for more on includes and extends.
Reuse template code with includes:
Basic Include
<div class="layout"> {% include "shared/header.html" %}
<main> <h1>{{page_title}}</h1> <div>{{content}}</div> </main>
{% include "shared/footer.html" %}</div>Include with Parameters
Pass specific data to included templates using with. Note that multiple parameters are separated by & (not commas):
{% for user in users %} {% include "user_card.html" with user=user & show_admin=true %}{% endfor %}
<!-- Alternative: include in navbar with avatar -->{% include "shared/navbar.html" with avatar_url=current_user.avatar_url %}user_card.html:
<div class="user-card"> <h3>{{user.name}}</h3> <p>{{user.email}}</p> {% if show_admin and user.is_admin %} <span class="admin-badge">Admin</span> {% endif %}</div>Context Variables
Templates automatically have access to additional context:
Built-in Context
<!-- JavaScript commands (always available) --><button phx-click="toggle" data-js="{{js | js.toggle('#sidebar')}}"> Toggle Sidebar</button>
<!-- LiveView metadata --><div data-live-view="{{live_view_id}}"> <!-- Template content --></div>Custom Context Processors
Add global template variables:
# In your application setupfrom pyview.template.context_processor import context_processor
@context_processordef add_app_context(meta): return { "app_name": "My PyView App", "version": "1.0.0", "current_year": datetime.now().year }Use in templates:
<footer> <p>© {{current_year}} {{app_name}} v{{version}}</p></footer>