Fragments

Render a single named template block on its own, so htmx can swap just the piece of the DOM it asked for.

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

What a fragment is

A fragment is a single named block from a template, rendered on its own — without the surrounding page. That is what lets an htmx request swap just the piece of the DOM it asked for: the browser requests a target, Chirp returns only that block.

This page covers the return types that produce fragments:

  • Fragment— render one named block.
  • Page— auto-pick fragment vs. full page based on the request.
  • OOB— one response, several out-of-band swaps.
  • ValidationError— a 422 form fragment for a failed validation.

If you already know htmx fragments and just want the Chirp mapping, jump to the example below. The return type is the intent — for the full picture of every return type, see the return-type decision tree.

Fragment

Fragmentrenders one named block from a template. The handler decides whether the request wants a fragment; the template stays the same single file that also serves the full page.

from chirp import Fragment, Request, Template

@app.route("/search")
def search(request: Request):
    results = do_search(request.query.get("q", ""))
    if request.is_narrow_fragment:
        return Fragment("search.html", "results_list", results=results)
    return Template("search.html", results=results)
{% extends "base.html" %}

{% block content %}
  <input type="search" hx-get="/search" hx-target="#results" name="q">

  {% block results_list %}
    <div id="results">
      {% for item in results %}
        <div class="result">{{ item.title }}</div>
      {% endfor %}
    </div>
  {% endblock %}
{% endblock %}

A full-page request renders everything (base layout + content + results). A narrow htmx request renders onlyresults_list — the <div id="results">and its contents.

Fragment(template, block, **context) takes the template path (relative to template_dir), the named block to render, and keyword arguments that become the rendering context. Pass target="dom-id"to override the swap target for an OOB or SSE delivery.

Detecting htmx requests

TheRequest object exposes typed properties for reacting to htmx requests. Use is_narrow_fragment for "should I return just the block?" — it is True only for a narrow fragment swap, and Falsefor boosted navigations and history restores, which need full page content.

Property Truewhen
request.is_htmx Any htmx request (HX-Requestheader present).
request.is_narrow_fragment A narrow htmx swap — excludes boosted navigations and history restores.
request.htmx_target_id Returns the target element id (no leading#), or None.
request.is_history_restore htmx is restoring from history (cache miss on back/forward).

Page

Most htmx-reachable routes do not need theif/else. Pageis the auto-negotiated form: it inspects the request and renders the right thing.

from chirp import Page, Request

@app.route("/search")
def search(request: Request):
    results = do_search(request.query.get("q", ""))
    return Page("search.html", "results_list", results=results)

Pagedoes not collapse to a simple "fragment or full page" boolean. It renders:

  • the full template for normal browser navigations and htmx history restores;
  • the named fragment block for narrow htmx requests;
  • a wider page block for boosted navigations, when you supply one (below).

OOB (out-of-band swaps)

Sometimes one action updates several parts of the page.OOBsends a primary fragment plus additional out-of-band fragments in a single response.

from chirp import OOB, Fragment, Request

@app.route("/cart/add", methods=["POST"])
async def add_to_cart(request: Request):
    item = await add_item(request)
    return OOB(
        Fragment("cart.html", "cart_items", items=get_cart()),
        Fragment("layout.html", "cart_count", count=cart_count()),
        Fragment("layout.html", "total_price", total=cart_total()),
    )

The first fragment is the primary swap target. Each additional fragment is rendered with hx-swap-oob="true" and an id matching its target (the block name by default, or Fragment(..., target="id")), so htmx swaps them into the right places.

ValidationError

ValidationErrorbundles the common htmx form pattern: validate server-side, re-render the form fragment with errors, and return a 422 status so htmx knows to swap the error content.

from chirp import ValidationError, Request
from chirp.validation import validate, required, email

RULES = {"email": [required, email]}

@app.route("/register", methods=["POST"])
async def register(request: Request):
    form = await request.form()
    result = validate(form, RULES)
    if not result:
        return ValidationError(
            "register.html", "form_errors",
            errors=result.errors, form=form,
        )
    # ... create the user

The form_errors block re-renders with a 422 status. On the client, hook it with hx-target-422 (or a custom htmx error handler). Pass retarget="#error-banner" to add an HX-Retargetheader so errors land in a different element than the trigger.

Where fragments come from

Chirp discovers block names, regions, and dependencies from the template at build time — it never hard-codes which blocks exist. That is what backs fragment validation inapp.check()and automatic OOB region discovery. You render blocks; you do not register them.

Next step

Push fragments to the browser in real time with Server-Sent Events — the same named blocks, streamed instead of swapped on request.