Fragments

Render named template blocks independently for htmx

The Key Innovation

Most Python frameworks treat templates as "render a full page, return a string." Chirp can render a named block from a template independently, without rendering the rest of the page.

This is what makes htmx integration seamless. The browser requests a fragment, the server returns just the block it needs.

Fragment

Fragmentrenders a specific block from a template:

from chirp import Fragment

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

Arguments:

The template:

{% 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 %}

Full page request renders everything (base layout + content + results). Fragment request renders only results_list -- the <div id="results">and its contents.

request.is_fragment

TheRequestobject detects htmx requests automatically:

request.is_fragment      # True if HX-Request header present
request.htmx_target      # Value of HX-Target header (e.g., "#results")
request.htmx_trigger     # Value of HX-Trigger header
request.is_history_restore  # True if htmx history restore

Page

Pageis syntactic sugar that auto-detects whether to return a full page or a fragment:

from chirp import Page

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

If request.is_fragment is True, it renders the block. Otherwise, it renders the full template. This eliminates the if/elsepattern.

When a route needs two fragment scopes, passpage_block_name:

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

OOB (Out-of-Band Swaps)

Sometimes a single action needs to update multiple parts of the page.OOBsends a primary fragment plus additional out-of-band fragments in one response:

from chirp import OOB, Fragment

@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 main response. Additional fragments are appended with hx-swap-oob="true", so htmx swaps them into the correct locations on the page.

ValidationError

A specialized fragment for form validation errors. Returns a 422 status:

from chirp import ValidationError

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

This renders the form_errors block with a 422 status code, which htmx can handle with hx-target-422or a custom error handler.

Block Availability

render_block()resolves inherited blocks. You can render a parent-defined page root from a child template, and child overrides still win inside that parent block.

{# child.html #}
{% extends "base.html" %}

{% block search_results %}
  <div id="results">...</div>
{% endblock %}
{# base.html #}
{% block page_root %}
  <section class="page-shell">
    {% block search_results %}{% endblock %}
  </section>
{% endblock %}

In this shape:

  • render_block("search_results")returns only the inner results fragment
  • render_block("page_root") returns the full page shell with the child's search_resultsblock injected

How Chirp Finds Blocks

Chirp uses Kida'stemplate_metadata()to introspect templates at build time. Block names, regions, and dependencies come from the AST — Chirp never hard-codes which blocks exist. That enables:

  • Validationfragment_block and page_blockare checked before render
  • OOB discovery — Blocks named*_oobare discovered automatically for app shells
  • Layout contractsdepends_on and cache_scopefrom each block drive when OOB regions are rendered

See Kida Integration for the full flow.

Regions for Shell OOB

{% region %}is the preferred pattern for app shell updates (breadcrumbs, sidebar, title). One definition serves both full-page slots and OOB swaps — no duplication.

Minimal layout example:

{% region breadcrumbs_oob(breadcrumb_items=[{"label":"Home","href":"/"}]) %}
{{ breadcrumbs(breadcrumb_items) }}
{% end %}

{% region title_oob(page_title="My App") %}
<title id="chirpui-document-title" hx-swap-oob="true">{{ page_title }}</title>
{% end %}

{% region sidebar_oob(current_path="/") %}
{{ sidebar(current_path=current_path) }}
{% end %}

{% call app_shell(brand="My App") %}
  {% slot topbar %}
  {{ breadcrumbs_oob(breadcrumb_items=breadcrumb_items | default([{"label":"Home","href":"/"}])) }}
  {% end %}
  {% slot sidebar %}
  {{ sidebar_oob(current_path=current_path | default("/")) }}
  {% end %}
  {% block content %}{% end %}
{% end %}

ChirpUI's breadcrumbs_oob, sidebar_oob, and title_oob map to chirpui-topbar-breadcrumbs, chirpui-sidebar-nav, and chirpui-document-title automatically. See Kida Integration for the full flow and examples/chirpui/shell_oobfor a complete reference.

Block-Heavy Layouts

Templates with many blocks (extends, nested blocks, fragments) benefit from a clear structure. Use the extension block pattern so child templates can add content without replacing parent layout.

Boost Layout Pattern

Extendchirp/layouts/boost.htmlfor htmx-boost + SSE apps. The layout defines stable blocks:

Block Purpose
title Page title
head Extra head content (styles, meta)
head_style Inline CSS (e.g. view-transition overrides)
body_before Content before#main
content Main content (inside#main, swapped on navigation)
sse_scope SSE connection (outside#mainso it persists)
body_after Scripts (event delegation, theme toggle)

content is swapped on navigation; sse_scope and body_after stay in place. Put event delegation and other scripts in body_afterso they run once and work for dynamically swapped content.

Extension Blocks

Since Kida doesn't supportsuper(), use explicit extension blocks in your base template:

{% block head %}
<link rel="stylesheet" href="/css/base.css">
{% block extra_head %}{% end %}
{% end %}

{% block body_after %}
<script src="/js/main.js"></script>
{% block extra_scripts %}{% end %}
{% end %}

Child templates override extra_head or extra_scriptsto add content without replacing the base.

Nesting for Fragments

Define fragment blocks inside the block that gets swapped. For example, ifpage_root is the page-level swap target, put results_list inside it so the fragment target (#results) exists in the DOM.

Next Steps