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).
Boosted navigation: a wider fragment root
When a route is reachable by both narrow swaps andhx-boost navigation, the narrow block is often too small to stand alone as a page body. Pass page_block_nameso boosted navigations get a fragment-safe root while explicit swaps still target the narrow block:
return Page(
"search.html",
"results_list",
page_block_name="page_root",
results=results,
)
results_liststays the narrow fragment target for explicit swaps.page_rootbecomes the fragment-safe root for boosted navigation.
For mounted page-directory templates that follow Chirp's conventionalpage_root / page_content blocks, Page.mounted("dashboard/page.html", **ctx)wires both names for you.
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.
How Chirp finds blocks
Chirp uses Kida'stemplate_metadata()to introspect each template's AST at build time. Block names, regions, and dependencies come from the AST, which enables:
- Validation —
Fragment/Pageblock names are checked before render. - OOB discovery — blocks named
*_oobare discovered automatically for app shells. - Layout contracts — each block's
depends_onandcache_scopedrive when OOB regions re-render.
This is build-time machinery you do not call directly. See Kida integration for the full flow.
Next step
Push fragments to the browser in real time with Server-Sent Events — the same named blocks, streamed instead of swapped on request.