Skip to content
djust
Appearance
Mode

Template Cheat Sheet

Quick reference for every directive, attribute, and Django tag used in djust templates.

Required Template Structure

Every LiveView template needs these two things:

{% load djust_tags %}
<!DOCTYPE html>
<html>
<head>
    {% djust_scripts %}   {# Loads ~5KB client JavaScript #}
</head>
<body dj-view="{{ dj_view_id }}">   {# Binds page to WebSocket session #}
    <div dj-root>                    {# Reactive region — only this is diffed/patched #}
        {{ count }}
        <button dj-click="increment">+</button>
    </div>
</body>
</html>
Attribute / TagRequiredDescription
{% load djust_tags %}YesLoad djust template tag library
{% djust_scripts %}YesInjects client JavaScript (~5KB)
dj-view="{{ dj_view_id }}"YesOn <body> — identifies the WebSocket session
dj-rootYesMarks the reactive subtree — only HTML inside is diffed

Event Directives

Click & Submit

AttributeFires OnHandler Receives
dj-click="handler"Clickdata-* attributes as kwargs
dj-submit="handler"Form submitAll named form fields as kwargs
dj-copy="text"ClickClient-only clipboard copy, no server round-trip
dj-copy="#selector"ClickCopy textContent of matched element
<!-- Simple click -->
<button dj-click="increment">+</button>

<!-- Pass data to handler -->
<button dj-click="delete" data-item-id="{{ item.id }}">Delete</button>

<!-- Inline args (positional) -->
<button dj-click="set_period('month')">Monthly</button>

<!-- Confirmation dialog before sending -->
<button dj-click="delete" dj-confirm="Are you sure?">Delete</button>

<!-- Form submit -->
<form dj-submit="save_form">
    {% csrf_token %}
    <input name="title" value="{{ title }}" />
    <button type="submit">Save</button>
</form>

<!-- Client-side clipboard copy (literal text) -->
<button dj-copy="{{ share_url }}">Copy link</button>

<!-- Copy from another element -->
<button dj-copy="#code-block">Copy Code</button>

<!-- Copy with feedback and server event -->
<button dj-copy="{{ api_key }}" dj-copy-feedback="Done!" dj-copy-event="copied">Copy</button>

dj-copy options

AttributeDescription
dj-copy-feedback="text"Button text shown for 2s after copy (default: "Copied!")
dj-copy-class="class"CSS class added for 2s after copy (default: dj-copied)
dj-copy-event="handler"Server event fired after successful copy

Input & Change

AttributeFires OnHandler Receives
dj-input="handler"Every keystrokevalue= current field value
dj-change="handler"Blur / select changevalue= current field value
dj-blur="handler"Focus leaves elementvalue= current field value
dj-focus="handler"Focus enters elementvalue= current field value
dj-model="field_name"Two-way bindingAuto-syncs self.field_name
<!-- Live search -->
<input type="text" dj-input="search" value="{{ query }}" />

<!-- Debounce via HTML attribute (preferred) -->
<input dj-input="search" dj-debounce="300" />

<!-- Throttle via HTML attribute -->
<button dj-click="poll" dj-throttle="500">Refresh</button>

<!-- Defer until blur -->
<input dj-input="validate" dj-debounce="blur" />

<!-- Disable default debounce on dj-input -->
<input dj-input="on_change" dj-debounce="0" />

<!-- Legacy data-* attributes (still supported) -->
<input dj-input="search" data-debounce="500" />
<input dj-input="on_resize" data-throttle="100" />

<!-- Select change -->
<select dj-change="filter_status">
    <option value="all">All</option>
    <option value="active">Active</option>
</select>

<!-- Two-way model binding -->
<input dj-model="username" type="text" />

Keyboard

<!-- Fire on Enter key -->
<input dj-keydown.enter="submit" />

<!-- Fire on Escape key -->
<input dj-keydown.escape="cancel" />

<!-- Fire on any keydown -->
<div dj-keydown="on_key" tabindex="0"></div>

Supported key modifiers: .enter, .escape, .space

Window & Document Events

AttributeTargetEvent
dj-window-keydown="handler"windowkeydown
dj-window-keyup="handler"windowkeyup
dj-window-scroll="handler"windowscroll (150ms throttle)
dj-window-click="handler"windowclick
dj-window-resize="handler"windowresize (150ms throttle)
dj-document-keydown="handler"documentkeydown
dj-document-keyup="handler"documentkeyup
dj-document-click="handler"documentclick
<!-- Close modal on Escape anywhere -->
<div dj-window-keydown.escape="close_modal">

<!-- Track scroll position -->
<div dj-window-scroll="on_scroll">

<!-- Detect background clicks -->
<div dj-document-click="on_click">

Key modifier filtering works: dj-window-keydown.escape="handler". The element provides context (dj-value-*, component ID) but the listener attaches to window/document.

Click Away

<!-- Fire event when user clicks outside this element -->
<div dj-click-away="close_dropdown" class="dropdown">
    ...
</div>

Uses capture-phase document listener (works even if inner elements call stopPropagation()). Supports dj-confirm and dj-value-*.

Keyboard Shortcuts

<!-- Single shortcut -->
<div dj-shortcut="escape:close_modal">

<!-- Multiple shortcuts, modifier keys -->
<div dj-shortcut="ctrl+k:open_search:prevent, escape:close_modal">

<!-- Modifiers: ctrl, alt, shift, meta (cmd on Mac) -->
<div dj-shortcut="ctrl+shift+s:save:prevent">

Syntax: [modifier+...]key:handler[:prevent] (comma-separated for multiple). The prevent suffix calls preventDefault(). Shortcuts skip form inputs by default; add dj-shortcut-in-input to override.

AttributeDescription
dj-patch="url"Replace dj-root content via AJAX (no full reload)
dj-navigate="url"Client-side navigation (history push)
dj-prefetchPrefetch link target on hover / touch — warms HTTP cache before click (v0.7.0)
<!-- Patch: replace reactive region only -->
<a dj-patch="{% url 'my_view' page=2 %}">Next page</a>

<!-- Navigate: full client-side navigation with history -->
<a dj-navigate="{% url 'dashboard' %}">Dashboard</a>

<!-- Prefetch on hover (65ms debounce) / touchstart (immediate) -->
<a dj-prefetch href="{% url 'dashboard' %}">Dashboard</a>

<!-- Opt out of prefetch on a specific link -->
<a dj-prefetch="false" href="/logout/">Log out</a>

See the prefetch guide for same-origin / data-saver / dedupe semantics.

Polling

<!-- Poll every 5 seconds (default) -->
<div dj-poll="refresh"></div>

<!-- Poll every 10 seconds -->
<div dj-poll="refresh" dj-poll-interval="10000"></div>

Submit Protection

AttributeDescription
dj-disable-with="text"Disable button + replace text during submission
dj-lockBlock event until server responds (prevents double-fire)
<!-- Disable + replace text while submitting -->
<button type="submit" dj-disable-with="Saving...">Save</button>

<!-- Lock to prevent concurrent events -->
<button dj-click="save" dj-lock>Save</button>

<!-- Combined: lock + visual feedback -->
<button dj-click="save" dj-lock dj-disable-with="Saving...">Save</button>

Lifecycle & Reconnection

AttributeFires OnHandler Receives
dj-mounted="handler"Element enters DOM (after VDOM patch)dj-value-* attrs as kwargs
dj-auto-recover="handler"WebSocket reconnectsForm values + data-* from container
dj-no-recoverOpts field out of automatic form recovery on reconnect
<!-- Fire event when element appears after a VDOM patch -->
<div dj-mounted="on_widget_ready" dj-value-widget-id="{{ widget.id }}">
    ...
</div>

<!-- Restore complex state after reconnection -->
<div dj-auto-recover="restore_state" dj-value-canvas-id="main">
    <input name="brush_size" value="5" />
</div>

<!-- Opt out of automatic form recovery -->
<input name="scratch" dj-change="on_change" dj-no-recover />

dj-mounted does not fire on initial page load — only after subsequent VDOM patches insert the element.

dj-auto-recover does not fire on initial page load — only after WebSocket reconnection. Serializes form field values and data-* attributes from the container.

dj-no-recover prevents a field from being auto-recovered on reconnect. Useful for ephemeral search fields or fields where server state is the source of truth. Fields inside dj-auto-recover containers are automatically skipped (custom handler takes precedence).


UI Feedback Attributes

Connection State CSS Classes

djust automatically applies CSS classes to <body> based on WebSocket/SSE connection state:

ClassApplied when
dj-connectedWebSocket/SSE connection is open
dj-disconnectedWebSocket/SSE connection is lost

Both classes are removed on intentional disconnect (e.g., TurboNav navigation). Use these for CSS-driven connection feedback:

/* Dim content when disconnected */
body.dj-disconnected dj-root { opacity: 0.5; }

/* Show an offline banner */
.offline-banner { display: none; }
body.dj-disconnected .offline-banner { display: block; }

dj-cloak (FOUC Prevention)

Hide elements until the WebSocket/SSE connection is established, preventing flash of unconnected content:

<!-- Hidden until mount response is received -->
<div dj-cloak>
    <button dj-click="increment">+</button>
</div>

The CSS rule [dj-cloak] { display: none !important; } is injected automatically by client.js. The dj-cloak attribute is removed from all elements when the mount response arrives.

Note: If the WebSocket never connects, cloaked elements stay hidden. Only cloak elements that are WebSocket-dependent.

dj-scroll-into-view (Auto-scroll on Render)

Automatically scroll an element into view after it appears in the DOM (via mount or VDOM patch):

<!-- Smooth scroll (default) -->
<div dj-scroll-into-view>New message</div>

<!-- Instant scroll (no animation) -->
<div dj-scroll-into-view="instant">Alert</div>

<!-- Scroll to center of viewport -->
<div dj-scroll-into-view="center">Highlighted item</div>

<!-- Scroll to start or end -->
<div dj-scroll-into-view="start">Section header</div>
<div dj-scroll-into-view="end">Latest entry</div>
ValueBehavior
"" (default){ behavior: 'smooth', block: 'nearest' }
"instant"{ behavior: 'instant', block: 'nearest' }
"center"{ behavior: 'smooth', block: 'center' }
"start"{ behavior: 'smooth', block: 'start' }
"end"{ behavior: 'smooth', block: 'end' }

One-shot per DOM node: each element scrolls only once. VDOM-replaced elements (fresh nodes) scroll again correctly.

Page Loading Bar

An NProgress-style thin loading bar at the top of the page during TurboNav and live_redirect navigation. Always active by default -- no opt-in attribute needed.

Control programmatically:

// Manual control
window.djust.pageLoading.start();
window.djust.pageLoading.finish();

// Disable entirely
window.djust.pageLoading.enabled = false;

Or hide via CSS:

.djust-page-loading-bar { display: none !important; }

Navigation lifecycle events and CSS class for page transitions:

/* CSS-only page transition (zero JS) */
[dj-root].djust-navigating main {
    opacity: 0.3;
    transition: opacity 0.15s ease;
    pointer-events: none;
}
// JS hooks for advanced use cases
document.addEventListener('djust:navigate-start', () => showSkeleton());
document.addEventListener('djust:navigate-end', () => hideSkeleton());

Loading States

Loading state directives apply CSS classes or show/hide elements while a server round-trip is in progress.

DirectiveDescription
dj-loadingToggle djust-loading class on the element itself
dj-loading.class:fooAdd class foo while loading
dj-loading.hideHide element while loading
dj-loading.showShow element only while loading (spinner pattern)
dj-loading.disableDisable element while loading
dj-loading.target=#idApply loading state to #id instead of current element
<!-- Button disables itself while request is in flight -->
<button dj-click="save" dj-loading.disable>Save</button>

<!-- Spinner appears only during loading -->
<button dj-click="generate">Generate</button>
<div dj-loading.show.target=#gen-btn id="spinner">Loading...</div>

<!-- Loading overlay on a card -->
<div dj-loading.class:opacity-50>
    {{ content }}
</div>

Passing Data to Handlers

data-* attributes

<!-- data-* attributes are coerced to their natural type -->
<button dj-click="select_item"
        data-item-id="{{ item.id }}"
        data-price="{{ item.price }}"
        data-active="true">
    Select
</button>

Handler receives: select_item(self, item_id=42, price=9.99, active=True)

Type coercion rules:

  • "true" / "false"bool
  • Numeric strings → int or float
  • Everything else → str

dj-value-* attributes

<!-- Pass extra values without data- prefix -->
<button dj-click="handler" dj-value-mode="edit" dj-value-row="{{ row.id }}">
    Edit
</button>

_target (automatic)

For dj-change and dj-input, the _target parameter is included automatically with the triggering element's name attribute. Useful when multiple fields share one handler:

<input name="email" dj-change="validate" />
<input name="username" dj-change="validate" />

Handler receives _target="email" or _target="username".


VDOM Identity

Reactive Region

<body dj-view="{{ dj_view_id }}">
    <div dj-root>
        <!-- Everything inside dj-root is managed by djust's VDOM -->
        <!-- Only this region is diffed and patched after events -->
    </div>
</body>

Rule: dj-root must contain all dynamic content. Static headers, navbars, and footers outside dj-root are never touched.

Keyed Lists

<!-- Without key: diffed by position (may produce extra DOM mutations) -->
{% for item in items %}
<div>{{ item.name }}</div>
{% endfor %}

<!-- With data-key: djust detects moves/inserts/removes optimally -->
{% for item in items %}
<div data-key="{{ item.id }}">{{ item.name }}</div>
{% endfor %}

<!-- With dj-key: same as data-key -->
{% for item in items %}
<li dj-key="{{ item.id }}">{{ item.name }}</li>
{% endfor %}

Use data-key or dj-key on list items whenever the list can reorder or items can be inserted/deleted. Analogous to React key.

Opt Out of Patching

<!-- External JS owns this subtree (charts, rich text editors, maps) -->
<div dj-update="ignore" id="my-chart"></div>

JavaScript Hooks

<div dj-hook="chart" id="my-chart"></div>
djust.hooks.chart = {
    mounted(el)   { initChart(el); },
    updated(el)   { updateChart(el); },
    destroyed(el) { destroyChart(el); },
};

Django Template Tags & Filters

Supported Tags

TagNotes
{{ variable }}Variable output (auto-escaped)
{% if %} / {% elif %} / {% else %} / {% endif %}Conditionals
{% for %} / {% empty %} / {% endfor %}Loops
{% url 'name' arg=val %}URL resolution
{% include "partial.html" %}Template includes
{% extends "base.html" %}Template inheritance
{% block %} / {% endblock %}Block overrides
{% load tag_library %}Load template tag library
{% csrf_token %}CSRF token
{% static 'file' %}Static file URL
{% with var=value %}Local variable assignment
{% dj_activity "name" visible=expr eager=expr %}...{% enddj_activity %}Pre-rendered hidden panel with preserved local state (React 19.2 parity). See Activity guide.
{% djust_markdown expr [kwargs] %}Render Markdown to sanitised HTML in the Rust parser — raw HTML and javascript: URLs are neutralised; trailing-line provisional wrap makes streaming LLM output flicker-free. See Streaming Markdown guide.

Comparison operators inside {% if %}

The Rust template engine accepts the full set of Python comparison operators inside {% if %} and {% elif %} conditions — not just == / !=:

{% if cart.total > 100 %}
  <span class="badge">free shipping</span>
{% endif %}

{% if user.age >= 18 and user.age < 65 %}{% endif %}
{% if rating <= 2 %}{% elif rating < 5 %}{% else %}{% endif %}

>, <, >=, <=, ==, !=, in, not in — all work as you'd expect. Combine with and / or / not. (Available since v0.1.6.)

{{ model.pk }} for Django model context

Pass a Django model instance into the template context and you can read its primary key directly:

class ArticleView(LiveView):
    article = state(default=None)

    def mount(self, request, slug):
        self.article = Article.objects.get(slug=slug)
<a href="{% url 'article-edit' pk=article.pk %}">Edit</a>

The Rust serializer auto-includes a pk key on every model instance regardless of the field name (id, uuid, custom). You can still read the underlying field by its real name (article.id, article.uuid) — pk is just the cross-model alias.

Custom Tag Handlers (register_tag_handler / register_block_tag_handler / register_assign_tag_handler)

Three registration entrypoints let you wire Python callbacks into the Rust template engine without forking the parser:

VarietyReturnsUse when
register_tag_handler(name, handler)HTML stringThe tag emits content ({% url %}, {% static %})
register_block_tag_handler(name, handler)HTML wrapping the inner blockThe tag wraps content ({% upper %}…{% endupper %})
register_assign_tag_handler(name, handler)dict[str, Any] merged into the contextThe tag mutates the context for sibling nodes ({% assign x=expr %})
from djust._rust import register_tag_handler

def hello_tag(args, context):
    name = args.get("name", "world")
    return f"<p>Hello, {name}!</p>"

register_tag_handler("hello", hello_tag)
{% hello name="Alice" %}

Overhead is ~100–500 ns per call (PyO3 boundary). Built-in tags (if, for, block, …) stay in pure Rust with zero overhead. See ADR-005 in the djust repo for the architecture rationale.

Auto-serialization for Django types

Django types pass through the Rust template engine without manual .isoformat() / .hex conversion:

Django typeRenders as
datetime.datetimeISO 8601 — works with |date:"Y-m-d H:i"
datetime.dateISO 8601
datetime.timeHH:MM:SS
decimal.Decimalstring (preserves precision; pair with |floatformat)
uuid.UUIDstring
FieldFile (FileField / ImageField)object — call .url, .name, .size directly

Pass them via context / self.*; the serializer handles the rest.

Filters (all 57 Django built-ins)

String

FilterExample
upper{{ name|upper }}"ALICE"
lower{{ name|lower }}
title{{ name|title }}
capfirst{{ text|capfirst }}
truncatechars:N{{ text|truncatechars:50 }}
truncatewords:N{{ text|truncatewords:20 }}
wordcount{{ text|wordcount }}
slugify{{ title|slugify }}
urlencode?q={{ query|urlencode }}
linebreaks{{ bio|linebreaks }}
linebreaksbr{{ bio|linebreaksbr }}
urlize{{ text|urlize }} — do not add |safe (handles own escaping)

Number

FilterExample
floatformat:N{{ price|floatformat:2 }}"9.99"
intcomma{{ count|intcomma }}"1,234"
filesizeformat{{ bytes|filesizeformat }}"1.2 MB"
pluralize{{ count }} item{{ count|pluralize }}

Date/Time

FilterExample
date:"Y-m-d"{{ created|date:"Y-m-d" }}
time:"H:i"{{ ts|time:"H:i" }}
timesince{{ created|timesince }}"3 days ago"
timeuntil{{ expires|timeuntil }}

List/Dict

FilterExample
length{{ items|length }}
first{{ items|first }}
last{{ items|last }}
join:", "{{ tags|join:", " }}
dictsort:"key"{{ items|dictsort:"name" }}
slice:":3"{{ items|slice:":3" }}

Logic

FilterExample
default:"fallback"{{ value|default:"—" }}
default_if_none:"N/A"{{ value|default_if_none:"N/A" }}
yesno:"yes,no,maybe"{{ flag|yesno:"enabled,disabled" }}

Escaping

FilterExampleNotes
safe{{ html|safe }}Mark pre-escaped HTML safe
escape{{ text|escape }}Force HTML escaping
force_escape{{ text|force_escape }}Escape even in {% autoescape off %}
striptags{{ html|striptags }}Remove all HTML tags

Common Pitfalls

One-sided {% if %} in class attributes

Problem: Using {% if %} without {% else %} inside an HTML attribute can confuse djust's branch-aware div-depth counter, causing VDOM patching misalignment.

<!-- WRONG: one-sided if inside class attribute -->
<div class="card {% if active %}active{% endif %}">

Fix: Use a separate attribute or a full {% if/else %} expression:

<!-- CORRECT: full if/else -->
<div class="card {% if active %}active{% else %}{% endif %}">

<!-- ALSO CORRECT: move the conditional outside -->
{% if active %}
<div class="card active">
{% else %}
<div class="card">
{% endif %}
    ...
</div>

This limitation applies specifically to class and other attribute values — {% if %} blocks in element content work fine.

Form field values during VDOM patch

djust's VDOM preserves text input values during patches by default. However, if the server re-renders a field with a different value= attribute, the new server value wins. To preserve a field that the user is actively editing, use dj-update="ignore" on its container:

<div dj-update="ignore">
    <input type="text" name="draft" />
</div>

Double-escaping HTML filters

urlize, urlizetrunc, and unordered_list are in djust's safe_output_filters whitelist — the Rust engine automatically marks their output as safe without requiring |safe. Do not pipe them through |safe or you'll double-escape:

<!-- WRONG: double-escapes the output -->
{{ text|urlize|safe }}

<!-- CORRECT: djust's Rust engine auto-marks urlize output as safe -->
{{ text|urlize }}

Note: Standard Django achieves this via SafeData type-checking. djust implements it as an explicit whitelist, so users coming from Django don't need |safe with these filters.

{% elif %} in inline templates

{% elif %} is not supported in template_string / template = inline templates. Use separate {% if %} blocks:

<!-- WRONG in inline templates -->
{% if a %}...{% elif b %}...{% endif %}

<!-- CORRECT -->
{% if a %}...{% endif %}
{% if not a and b %}...{% endif %}

Quick Reference Card

Event attributes:
  dj-click        dj-submit       dj-change       dj-input
  dj-blur         dj-focus        dj-keydown      dj-keyup
  dj-poll         dj-patch        dj-navigate     dj-copy
  dj-confirm      dj-model        dj-mounted      dj-auto-recover
  dj-click-away   dj-shortcut     dj-no-recover

Window/document scoping:
  dj-window-keydown               (keydown on window)
  dj-window-keyup                 (keyup on window)
  dj-window-scroll                (scroll on window, 150ms throttle)
  dj-window-click                 (click on window)
  dj-window-resize                (resize on window, 150ms throttle)
  dj-document-keydown             (keydown on document)
  dj-document-keyup               (keyup on document)
  dj-document-click               (click on document)

Rate limiting (HTML attributes):
  dj-debounce="300"               (debounce ms, per element)
  dj-debounce="blur"              (defer until blur)
  dj-debounce="0"                 (disable default debounce)
  dj-throttle="500"               (throttle ms, per element)

Copy enhancements:
  dj-copy="#selector"             (copy element textContent)
  dj-copy-feedback="Done!"        (custom feedback text, 2s)
  dj-copy-class="btn-success"     (custom CSS class, 2s)
  dj-copy-event="handler"         (server event after copy)

Submit protection:
  dj-disable-with="text"          (disable + replace text during submit)
  dj-lock                         (block event until server responds)

Loading directives:
  dj-loading                      (toggle djust-loading class)
  dj-loading.class:foo            (add class foo)
  dj-loading.hide                 (hide while loading)
  dj-loading.show                 (show only while loading)
  dj-loading.disable              (disable while loading)
  dj-loading.target=#id           (apply to target element)

UI feedback:
  dj-cloak                        (hide until WS/SSE mount completes)
  dj-scroll-into-view             (auto-scroll on render, smooth default)
  dj-scroll-into-view="instant"   (auto-scroll, no animation)
  dj-scroll-into-view="center"    (auto-scroll to viewport center)

Connection state (auto on <body>):
  .dj-connected                   (body class when connected)
  .dj-disconnected                (body class when disconnected)

Reconnection UI (auto on <body>):
  data-dj-reconnect-attempt       (current attempt number)
  --dj-reconnect-attempt          (CSS custom property, attempt number)
  .dj-reconnecting-banner         (auto-shown banner with attempt count)

Page loading bar:
  Always active for TurboNav / live_redirect
  window.djust.pageLoading.start/finish  (manual control)
  .djust-navigating             (on [dj-root] during navigation)
  djust:navigate-start          (CustomEvent on document)
  djust:navigate-end            (CustomEvent on document)

Document metadata (Python-side, no template directive):
  self.page_title = "..."              (update document.title)
  self.page_meta = {"key": "value"}    (update/create <meta> tags)

VDOM identity:
  dj-view="{{ dj_view_id }}"      (on body — required)
  dj-root                         (reactive region — required)
  data-key / dj-key               (stable list identity)
  dj-update="ignore"              (opt out of patching)
  dj-hook="name"                  (JS lifecycle hooks)

Data passing:
  data-*                          (typed kwargs to handlers)
  dj-value-*                      (extra value kwargs)
  dj-target="#selector"           (scoped DOM updates)