UI layers & shell regions

A shared vocabulary for app shell, page chrome, and surface chrome, plus swap_attrs for resolving hx-target from route geometry

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

Overview

A persistent app shell is a fixed topbar and sidebar that stays put while the inner content swaps. The moment you have a shell, every link needs an answer to one question: how wide does this swap go? Does clicking it repaint the whole shell, just the page body, or only a panel inside the page?

This page gives you a shared name for each of those widths — app shell, page chrome, surface chrome — and shows how to author links with theswap_attrs template global so Chirp resolves the right hx-targetfrom your route layout, instead of you hard-coding DOM ids on every link.

The four layers

Chirp and chirp-ui use the words shell and chrome a lot. Here is the one vocabulary the docs, the APIs, and your templates all share. Read it top-down: each layer swaps a narrower slice of the page than the one above it.

Layer Name Swap scope Where it lives Updates on
L1 App shell shell Topbar, sidebar, the#main wrapper — everything outside #page-content Boosted navigation plus out-of-band swaps for shell regions
L2 Shell outlet shell #page-content, inside #main Boosted navigation (viahx-select)
L3 Page chrome page Headers, tabs, and toolbars inside#page-content Page-level fragment targets such as#page-root
L4 Surface chrome content The border, padding, and scroll area around a single component (a card, panel, or bento cell) Local swaps only — never the app shell

Resolve the target withswap_attrs

Each layout level can declare a swap scope: a stable name for "how wide a swap happens here." Scopes let you author a link once and have Chirp pick thehx-target, instead of memorizing which DOM id is correct from each page.

Theswap_attrs(href) template global computes the nearest shared navigation boundary between the current path and the destination, then returns the hx-target (and hx-boost) for that link. Pipe it through html_attrs to render the attributes. An explicit hx-targeton the element always wins.

<!-- Hand-coded: you must know the right id for every link -->
<a href="/dashboard/reports"
   hx-target="#main"
   hx-boost="true">Reports</a>
<!-- swap_attrs: Chirp resolves the target from the route layout chain -->
<a href="/dashboard/reports"
   {{ swap_attrs("/dashboard/reports") | html_attrs }}>Reports</a>

When chirp-ui is active,swap_attrsis registered for you and bound to these three well-known scopes:

Scope Typical role Default target id
shell L1–L2: swap inside site chrome main
page L3: tabbed or page chrome page-root
content L4: narrow in-page swap page-content-inner

Register a custom scope

Apps can add their own scope name beyond the three defaults. Register it during setup, mapping the scope to a concrete target id (no#prefix — Chirp strips it):

app.register_swap_scope("section", "section-outlet")

A two-domain nested shell

A common layout is a marketing site that wraps a chirp-ui app: a public outer shell with its own header and footer, and an inner application shell with a sidebar. Each declares its own navigation domain, andswap_attrsreads the domains to decide where a swap lands:

  1. Outer site shell (pages/_layout.html, {# domain: site #}) — site header, footer, and a #site-contentoutlet.
  2. Inner app shell (pages/app/_layout.html, {# domain: app #}) — the chirp-ui app_shell() with sidebar, breadcrumbs, and shell_outlet().

Navigating from/ to /app swaps at the site level (#site-content). Navigating within /app/* swaps at the app level (the inner outlet). swap_attrs derives both from the layout chains — you do not set hx-targetby hand.

Shell regions (stable OOB targets)

Shell regions are elements with fixedid attributes that htmx updates with out-of-band swaps after the primary #main swap — the document title, route-scoped topbar actions, breadcrumbs. Chirp ships canonical ids in the chirp.shell_regionsmodule so tooling and tests agree on the spelling:

Constant Default element id Role
DOCUMENT_TITLE_ELEMENT_ID chirpui-document-title The<title>element
SHELL_ACTIONS_TARGET chirp-shell-actions Route-scoped topbar actions

SHELL_ELEMENT_IDS is a frozenset of those documented ids. Apps may add their own OOB targets (sidebars, extra breadcrumb trails); register them with app.register_oob_region(...)and document the ids locally.

A fragment target'striggers_shell_update flag controls whether swapping it reruns shell negotiation (the shell_actions OOB pass). It defaults to True; set it False for narrow in-page swaps — for example #page-content-inner— that should not refresh the topbar.

When a swap targets a specific scope (saypage), layout OOB blocks fire only for layouts at or above that scope's depth: the matched shell and its ancestors, never sibling or descendant shells. Boosted navigation with no explicit hx-targetfires OOB at every level.

Validate at startup

app.check()validates your scope and layout metadata at freeze time and reports duplicate swap scopes, missing outlets, frame-versus-swap conflicts, and conflicting outlets before they ship.

Advanced