Custom Template Tags & Filters
Extend the template language with your own filters, simple tags and inclusion tags.
When a template needs logic the built-in tags can't express — formatting a value a particular way, rendering a reusable widget, or querying a small bit of data — you write your own tag or filter. They live in a templatetags/ package inside an app and are pulled in with {% load %}.
Three flavours, in order of how often you'll reach for them
- Filter — transforms a single value:
{{ price|money }}. - Simple tag — runs a function and outputs the result:
{% active_url 'home' %}. - Inclusion tag — renders a small sub-template with its own context:
{% pagination page_obj %}.
# myapp/templatetags/money.py
from django import template
register = template.Library()
@register.filter
def money(value):
return f'${value:,.2f}'
# In a template:
# {% load money %}
# {{ product.price|money }} -> $1,299.00The templatetags folder must contain an __init__.py, and the app must be in INSTALLED_APPS, or {% load %} won't find your library.
Example
# blog/templatetags/blog_extras.py
from django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.db.models import Count
register = template.Library()
# 1) A filter with an argument.
@register.filter
def truncate_smart(text, length=50):
text = str(text)
return text if len(text) <= length else text[:length].rsplit(' ', 1)[0] + '\u2026'
# 2) A simple tag that needs the request context (highlight the active nav link).
@register.simple_tag(takes_context=True)
def active(context, url_name):
from django.urls import reverse
request = context['request']
return 'active' if request.path == reverse(url_name) else ''
# 3) A filter that safely builds HTML from user data.
@register.filter
def badge(label, kind='default'):
# format_html ESCAPES label — safe even if it contains <script>.
return format_html('<span class="badge badge-{}">{}</span>', kind, label)
# 4) An inclusion tag: a reusable, self-contained widget.
@register.inclusion_tag('blog/_tag_cloud.html')
def tag_cloud(limit=20):
tags = (
Tag.objects
.annotate(n=Count('posts'))
.filter(n__gt=0)
.order_by('-n')[:limit]
)
return {'tags': tags}
# Usage in a template:
# {% load blog_extras %}
# <a class="{% active 'home' %}" href="/">Home</a>
# {{ post.excerpt|truncate_smart:80 }}
# {{ 'Beta'|badge:'warning' }}
# {% tag_cloud limit=15 %}When to use it
- A developer creates a custom template tag to render a sidebar widget with its own database query, keeping views clean.
- A team writes a custom filter to convert Markdown text to safe HTML for use throughout all templates.
- A developer creates an inclusion tag to render a reusable notification badge component with its own context.
More examples
Simple custom filter
Registers a custom filter that estimates reading time from word count for use as {{ post.body|reading_time }}.
# blog/templatetags/blog_extras.py
from django import template
register = template.Library()
@register.filter(name="reading_time")
def reading_time(text):
words = len(text.split())
minutes = max(1, words // 200)
return f"{minutes} min read"Simple tag that returns a value
Outputs the site name from settings; simpler than a context processor for single-use template values.
from django import template
from django.conf import settings
register = template.Library()
@register.simple_tag
def site_name():
return getattr(settings, "SITE_NAME", "My Site")
# Usage in template: {% site_name %}Inclusion tag with own context
Renders a categories partial template with its own queryset, callable from any template without view changes.
from django import template
from ..models import Category
register = template.Library()
@register.inclusion_tag("blog/partials/categories.html")
def show_categories():
return {"categories": Category.objects.all()}
# Usage: {% show_categories %}
Discussion