Layout Patterns

Block-heavy layouts, boost pattern, and composition

4 min read 872 words

Overview

Chirp templates use Kida's block system:{% extends %}, {% block %}, {% include %}, and {% call %}. This guide covers patterns for block-heavy layouts and when to use each construct.

Choosing a Layout

Pick the right base layout before writing any templates.

For fixed column ratios on page content (grid() + block(span=…)), see the Chirp UI package docs/LAYOUT-OVERFLOW.md (section Fixed columns: grid presets).

Scenario Layout
App shell with sidebar navigation chirpui/app_shell_layout.html
htmx-boosted multi-page app chirp/layouts/boost.html
Fragment-only app (LLM, RAG, forms only) chirp/layouts/shell.html
Custom layout from scratch Write your own_layout.html

The critical distinction is whether your app includes a globalhx-selecton the main container:

  • boost.html sets hx-select="#page-content" on <main>. Every response is filtered through this selector — which is correct for boosted navigation but silently discards fragment responses that don't contain #page-content.
  • shell.html sets no hx-select. Fragment responses flow exactly where their hx-targetsays, with no filtering.
  • app_shell_layout.html uses per-link hx-boost on sidebar links rather than a container-level hx-select, so it avoids the issue entirely for navigation while leaving fragment forms unaffected.

chirp check detects the mismatch automatically via the select_inheritance rule and warns when a mutating element may silently discard its response due to inherited hx-select.

Boost Layout

Thechirp/layouts/boost.htmllayout is the recommended base for htmx-boost + SSE apps:

{% extends "chirp/layouts/boost.html" %}
{% block title %}My App{% end %}
{% block content %}
  <p>Page content goes here.</p>
{% end %}
{% block body_after %}
  <script>/* app-specific JS */</script>
{% end %}

Structure:

  • #main— htmx-boost target; gets replaced on navigation
  • body_before — above #main(e.g. nav bar)
  • content — inside #main; page-specific HTML
  • sse_scopeoutside #main; SSE connections persist across navigations
  • body_after— scripts, analytics

Important: Putsse_scope outside #main. If it's inside content, it gets replaced on navigation and live updates stop.

Shell Layout

Thechirp/layouts/shell.htmllayout is the base for fragment-only apps — pages where interactions trigger targeted fragment swaps rather than full-page boosted navigation:

{% extends "chirp/layouts/shell.html" %}
{% block title %}My App{% end %}
{% block content %}
  <p>Page content goes here.</p>
{% end %}
{% block body_after %}
  <script>/* app-specific JS */</script>
{% end %}

Unlike boost.html, shell.html sets no hx-select on <main>. Forms, SSE connections, and buttons can target any element on the page without interference.

Useshell.htmlwhen your app:

  • ReturnsFragment, OOB, or ValidationErrorto specific named targets
  • Does not use htmx-boosted sidebar navigation
  • Is an LLM playground, RAG UI, dashboard form, or other targeted-swap UI

Blocks:title, head, body_before, content, sse_scope, body_after — identical to boost.htmlfor easy migration.

Thehx-selectInheritance Sharp Edge

When usingboost.html, the global hx-select="#page-content" on <main> silently discards fragment responses that don't contain a #page-contentelement. The server returns 200 OK, htmx processes the response, finds no match for the selector, and swaps in nothing. The debug panel will report "Empty hx-select."

Symptoms:

  • Form submits return 200 OK but the UI never updates
  • The HTMX debug overlay shows "Empty hx-select" for the triggering element
  • Changinghx-targethas no effect

Fix: Switch fragment-only apps toshell.html. This is a one-line change in your base template:

{# Before: global hx-select causes silent empty swaps for forms #}
{% extends "chirp/layouts/boost.html" %}

{# After: no global hx-select, fragments flow where hx-target says #}
{% extends "chirp/layouts/shell.html" %}

Once on shell.html, remove any defensive hx-disinherit="hx-select"attributes that were working around the inherited selector — they are no longer needed.

Outer vs Inner Content

For SSE swap targets and fragment structure:

  • Outer element — Thesse-swaptarget. Holds padding, border, layout. Stays in the DOM; its innerHTML is replaced.
  • Inner element — The fragment block content. No duplicate padding or border.
<!-- Outer: swap target, has padding/border; hx-target="this" when sse-connect has hx-disinherit -->
<div class="answer" sse-swap="answer" hx-target="this">
  <!-- Inner: fragment renders this; no extra padding -->
  <div class="answer-body" data-copy-text="...">
    <div class="answer-content prose">...</div>
    <button class="copy-btn">Copy</button>
  </div>
</div>

Avoid nesting two elements with the same padding/border — it causes double spacing. Keep .copy-btn in normal flow (no position: absolute) so it stays with its answer.

When to Use Each Construct

Construct Use for
{% extends %} Base layout (boost, custom shell). One per template.
{% block %} Overridable sections. Child templates fill or extend.
{% include %} Reusable partials (headers, footers, cards). No block params.
{% call %} Macros with parameters. Use with{% def %}.

Blocks define slots; includes pull in full partials; call/def are parameterized components.

Block Inheritance

Child templates override blocks by redefining them:

{% extends "base.html" %}
{% block content %}
  {{ super() }}
  <p>Additional content after parent block.</p>
{% end %}

{{ super() }}renders the parent block's content. Omit it to replace entirely.

Next Steps

  • Fragments — Block-level rendering for htmx
  • App Shells — Persistent sidebar layout with SPA navigation
  • RAG Demo — Full layout example with SSE andshell.html