Why this page exists
Chirp + chirp-ui use several overlapping words (shell, chrome, fragments, scopes). This guide fixes one vocabulary for docs, APIs, and templates.
The four layers
| Layer | Official term | Symbolic scope | Where it lives | Updates when |
|---|---|---|---|---|
| L1 | App shell | shell |
Topbar, sidebar, wrapper#main — outside #page-content |
Boosted navigation + OOB for shell regions |
| L2 | Shell outlet | shell |
#page-content inside #main |
Boosted navigation (hx-select) |
| L3 | Page chrome | page |
Inside#page-content(headers, tabs, route toolbars) |
Broader fragment targets (e.g.#page-root) |
| L4 | Surface chrome | content |
Borders/padding/scroll around components (cards, panels, bento cells) | Local swaps only — not the app shell |
Rule: In prose and APIs, shell means L1 (persistent frame). Do not call card borders "shell"; use surface chrome.
Symbolic swap scopes
Each layout level can declare a symbolic swap scope — a stable name for "what width of swap happens at this level." Scopes decouple link authoring from DOM id details.
| Scope name | Typical role | Default target id (chirp-ui) |
|---|---|---|
shell |
L1–L2: swap inside site chrome | #main |
page |
L3: tabbed/page chrome | #page-root |
content |
L4: narrow in-page swap | #page-content-inner |
Apps can define custom scope names (e.g.sectionfor a showcase shell) beyond these three well-known defaults. Register them during setup:
app.register_swap_scope("section", "showcase-outlet")
Layout comments
Filesystem_layout.htmlfiles declare scope metadata via template comments:
{# target: body #}
{# domain: site #}
{# swap_scope: shell #}
{# outlet: site-content #}
{# frames: site-header, site-footer #}
| Comment | Purpose |
|---|---|
{# target: id #} |
DOM element this layout renders into (required) |
{# domain: name #} |
Explicit navigation-domain boundary forswap_attrs() |
{# swap_scope: name #} |
Symbolic scope forresolve_navigation_swap |
{# outlet: id #} |
Primary navigation outlet id for this level |
{# frames: id, id #} |
Immutable frame ids (contract-checked: must not be swap targets) |
swap_attrstemplate global
Instead of hand-codinghx-target on every link, use swap_attrsto resolve the correct target from route geometry:
{# Before: author must know the right id #}
<a href="/showcase/products" hx-target="#main" hx-boost="true">Products</a>
{# After: framework resolves from current path + destination layout chain #}
<a href="/showcase/products" {{ swap_attrs("/showcase/products") | html_attrs }}>Products</a>
swap_attrs(href)computes the nearest shared navigation boundary between the current
path and destination, then returns{"hx-target": "#main", "hx-boost": "true"}
(or broader/narrower, depending on context). When layouts declare{# domain: #},
domain ancestry drives the decision. Otherwise Chirp falls back to legacy shell/layout
geometry. Pipe throughhtml_attrsto render as HTML attributes. Explicit
hx-targeton the element always overrides the resolved value.
Nested shell example (b-site)
b-site uses two nested shells and two explicit navigation domains:
- Root marketing shell / site domain (
pages/_layout.html,{# domain: site #}) — site header, footer,#site-contentoutlet - Showcase app shell / showcase domain (
pages/showcase/_layout.html,{# domain: showcase #}) — chirp-uiapp_shell()with sidebar, breadcrumbs,shell_outlet()
Navigation from/ to /showcase swaps at the site level (#site-content). Navigation within /showcase/* swaps at the showcase level (inner outlet). swap_attrsderives this automatically from the layout chains.
Boosted navigation andhx-select
#mainparticipates in boosted navigation; the fragment selector depends on the layout:
| Layout | hx-select on #main |
Required in page HTML |
|---|---|---|
chirpui/app_shell_layout.html |
#page-content |
<div id="page-content">… wrapper with #page-rootinside |
chirpui/app_shell.htmlcustom shells |
#page-content via shell_outlet() |
shell_outlet()wraps the routed content block |
chirp/layouts/boost.html |
#page-content |
The layout's#page-contentwrapper |
For filesystem apps, root_layout.html should declare {# outlet: main #} (with {# target: body #}) so Chirp resolves HX-Target: #main and returns full shell HTML including #page-content. See Filesystem routing.
page_root remains the broader page-level fragment target inside the shell outlet. A block named page_root does not create id="page-root"; omitting that element breaks #page-root swaps even when boosted navigation still works through #page-content.
Shell regions (stable DOM ids)
Shell regions are elements with fixedid attributes that htmx updates with out-of-band swaps after the primary #main swap. Import canonical ids from :mod:chirp.shell_regions:
| Constant | Default element id | Role |
|---|---|---|
DOCUMENT_TITLE_ELEMENT_ID |
chirpui-document-title |
Document title (<title>) |
SHELL_ACTIONS_TARGET |
chirp-shell-actions |
Route-scoped topbar actions |
SHELL_ELEMENT_IDSis a frozenset of documented ids for tooling and tests.
Apps may add more OOB targets (breadcrumbs, sidebars); register them in the layout contract and document them locally.
Fragment targets, scopes, and OOB
Registered fragment targets (FragmentTargetRegistry) map HX-Target to Kida blocks. Each target may declare a scope_nametying it to a symbolic scope level.
triggers_shell_update means: "this swap may change shell regions; run shell negotiation (including shell_actions OOB)." Set it False for narrow in-page swaps (e.g. #page-content-inner) that should not refresh the topbar.
Scoped OOB: When a swap targets a specific scope (e.g.page), layout OOB blocks fire only for layouts at or above that scope's depth — the matched shell and its ancestors, not sibling or descendant shells. Boosted navigation (no explicit hx-target) fires OOB at all levels.
Contract checks
app.check()validates scope metadata at freeze time:
- Duplicate swap_scope per layout chain (two layouts claiming the same scope name)
- Missing outlet —
outlet_target_iddeclared but noid="..."found in any template - Frame/swap conflict — a
frame_targetsid is also registered as a fragment swap target - Conflicting outlets — two layouts at the same depth declare different
outlet_target_idvalues
HTMX ordering note
htmx applies the primary swap before out-of-band fragments. chirp-ui'sapp_shell_layout.html includes a small htmx:beforeSwap handler that clears #chirp-shell-actionswhen the response contains a matching OOB, avoiding one frame of stale actions. See chirp-ui app shell docs for details.
See also
- App shells — navigation model and
use_chirp_ui - chirp-ui App Shell — layouts and components
- Shell, sections, and route tabs (checklist) — chirp-ui doc: sections,
TabItem,hx-target, and OOB handoffs