Overview
chirp-ui is the official component library for Chirp: a set of
Kida template macros — cards, forms, modals,
layouts, an app shell — that render to styled HTML with no build step and no
client framework. Reach for it when you want a good-looking app out of the box
and want interactivity to come from htmx swaps
and native HTML (<dialog>, <details>) rather than a JavaScript framework.
Install thechirp[ui] extra, call use_chirp_ui(app), and import the macros
you need — the framework wires up the CSS, themes, and template filters for you.
It gives you:
- A full visual design out of the box. Override
--chirpui-*CSS variables to customize. - htmx-native interactivity. Components use htmx or native HTML (
<dialog>,<details>) — no client-side framework. - Composable macros.
{% slot %}for content injection; components nest freely. - Modern CSS.
:has(), container queries, fluid typography, andprefers-color-schemedark mode.
Installation
New in 0.11.0pip install "bengal-chirp[ui]"
# or
uv add "bengal-chirp[ui]"
pip install chirp-ui
# or
uv add chirp-ui
Setup
- 1
Wire chirp-ui into your app
Call
use_chirp_ui(app)after creating the app. It serveschirpui.css, themes, and transitions, and registers the filters chirp-ui components need (bem,field_errors,html_attrs,validate_variant).from chirp import App, AppConfig, use_chirp_ui app = App(AppConfig(template_dir="templates")) use_chirp_ui(app)use_chirp_uiships with thechirp[ui]extra. Iffrom chirp import use_chirp_uifails on an older Chirp, import it fromchirp.ext.chirp_uiinstead. - 2
Include the CSS in your base template
<link rel="stylesheet" href="/static/chirpui.css">For View Transitions, also add:
<link rel="stylesheet" href="/static/chirpui-transitions.css">
Template auto-detection
When chirp-ui is installed, Chirp's template loader adds the chirp-ui package
automatically. No configuration is needed for{% from "chirpui/..." %}
imports —chirpui/layout.html, chirpui/card.html, and the rest resolve from
the package.
Quick example
A two-card grid in a centered container:
{% from "chirpui/layout.html" import container, grid, block %}
{% from "chirpui/card.html" import card %}
{% call container() %}
{% call grid(cols=2) %}
{% call block() %}{% call card(title="Hello") %}<p>Card one.</p>{% end %}{% end %}
{% call block() %}{% call card(title="World") %}<p>Card two.</p>{% end %}{% end %}
{% end %}
{% end %}
App shell
The fastest way to a sidebar-and-topbar app is to extend chirp-ui's
app_shell_layout.htmland fill its blocks. No manual HTML boilerplate, and the
htmx-boost navigation contract is already wired:
{# target: body #}
{% extends "chirpui/app_shell_layout.html" %}
{% block brand %}My App{% end %}
{% block sidebar %}
{% from "chirpui/sidebar.html" import sidebar, sidebar_link, sidebar_section %}
{% call sidebar() %}
{% call sidebar_section("Main") %}
{{ sidebar_link("/", "Home") }}
{{ sidebar_link("/items", "Items") }}
{% end %}
{% end %}
{% end %}
Build the shell by hand
For full control, compose the shell from individual chirp-ui macros —
sidebar, breadcrumbs, and command_palette — in a standalone _layout.html.
The boost attributes on<main> are what make plain <a href>links navigate
without a full reload.
{# target: main #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Dashboard</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<link rel="stylesheet" href="/static/chirpui.css">
</head>
<body>
{% from "chirpui/sidebar.html" import sidebar, sidebar_section, sidebar_link %}
{% from "chirpui/breadcrumbs.html" import breadcrumbs %}
{% from "chirpui/command_palette.html" import command_palette, command_palette_trigger %}
{% from "chirpui/toast.html" import toast_container %}
{% set cp = current_path | default("/") %}
<div class="chirpui-app-shell">
<header class="chirpui-app-shell__topbar">
<a href="/" class="chirpui-app-shell__brand">My App</a>
<div class="chirpui-app-shell__topbar-center">
{{ breadcrumbs(breadcrumb_items | default([{"label": "Home", "href": "/"}])) }}
</div>
<div class="chirpui-app-shell__topbar-end">
{{ command_palette_trigger() }}
</div>
</header>
<aside class="chirpui-app-shell__sidebar">
{% call sidebar() %}
{% call sidebar_section("Navigate") %}
{{ sidebar_link("/", "Home", active=cp == "/") }}
{{ sidebar_link("/items", "Items", active=cp.startswith("/items")) }}
{% end %}
{% end %}
</aside>
<main id="main" class="chirpui-app-shell__main" tabindex="-1"
hx-boost="true" hx-target="#main"
hx-swap="innerHTML" hx-select="#page-content">
<div id="page-content">
{% block content %}{% end %}
</div>
</main>
</div>
{{ command_palette(search_url="/search") }}
{{ toast_container() }}
</body>
</html>
The boost contract here — hx-target="#main", hx-swap="innerHTML",
hx-select="#page-content"— keeps the shell chrome untouched while only the
inner fragment swaps. The reasoning behind each attribute lives in
Boosted navigation.
Nested (inner) shells
For layouts within layouts — e.g. a forum that frames each subforum — wrap a
region with theshell_sectionmacro:
{% from "chirp/macros/shell.html" import shell_section %}
{% call shell_section("forum-content") %}
{% block content %}{% end %}
{% end %}
Page spacing for boosted navigation
Let the page-level wrapper own vertical rhythm: a parent layout with a
page_root block holding container() + stack(gap="lg"), and inner blocks
likepage_contentfor the page-specific sections. Pair that with a wide page
block so boosted navigation swaps the full page shell, not a too-narrow inner
fragment:
return Page("dashboard.html", "page_content", page_block_name="page_root", **ctx)
For explicit fragment/page/region composition, PageCompositionexposes the
same idea viafragment_block= and page_block=. See
return types for when each applies.
Migrating from boost.html
Replace{% extends "chirp/layouts/boost.html" %}with
{% extends "chirpui/app_shell_layout.html" %}, then add {% block brand %},
{% block sidebar %}, and the other shell blocks. The hx-select="#page-content"
andid="page-content"are already in place.
If your app uses forms or SSE but no sidebar navigation, extend
chirp/layouts/shell.html instead. Unlike boost.html, shell.htmlsets no
globalhx-select, so fragment responses flow directly to their hx-target
with no risk of silent empty swaps — see
Shells.
Route tabs
To drive a tab bar from your route structure, registerSection.tab_itemsin
Python and setRouteMeta.section in each route's _meta.py. Chirp injects
tab_items / route_tabsinto the template context and registers the
tab_is_active helper. The render_route_tabsmacro is provided by the
chirp-ui package. See the
shell, sections, and route-tabs contract
for targets, boost behavior, andapp.check()expectations.
Component categories
| Category | Macros |
|---|---|
| Layout | container, grid, stack, block, page_header, section_header, divider, breadcrumbs, navbar, sidebar, hero, surface, callout |
| UI | card, card_header, modal, drawer, tabs, accordion, dropdown, popover, toast, table, pagination, alert, button_group |
| Forms | text_field, password_field, textarea_field, select_field, checkbox_field, toggle_field, radio_field, file_field, date_field, csrf_hidden, form_actions, login_form, signup_form |
| Data display | badge, spinner, skeleton, progress, description_list, timeline, tree_view, calendar |
| Streaming | streaming_block, copy_btn, model_card — for htmx SSE and LLM UIs |
See the chirp-ui repository for the full component reference and API.
Data layout patterns
For dashboard and settings pages, these patterns give consistent structure.
Section with header actions
Put section-level buttons (Refresh, Auto-detect, Run validation) in the
sectionactions slot, not beneath the content:
{% from "chirpui/layout.html" import section %}
{% from "chirpui/button.html" import btn %}
{% call section("Setup targets", subtitle="Configure your IDE") %}
{% slot actions %}{{ btn("Refresh", attrs_map={"hx-get": "/status", "hx-target": "#targets"}) }}{% end %}
<div id="targets">...</div>
{% end %}
Settings rows
For label + status + value (e.g. setup targets, health checks), use
settings_row_list and settings_row:
{% from "chirpui/settings_row.html" import settings_row_list, settings_row %}
{% call settings_row_list() %}
{{ settings_row("Cursor IDE", status="Configured", detail="setup cursor") }}
{{ settings_row("Skills directory", status="ok", detail="/path/to/skills") }}
{% end %}
Use description_listfor term + detail only (no status badge); use
settings_row_listwhen you have label + status + detail.
Theming
chirp-ui usesprefers-color-scheme for dark mode. Override any --chirpui-*
variable:
:root {
--chirpui-accent: #7c3aed;
--chirpui-container-max: 80rem;
}
For a manual light/dark toggle, set data-theme="light" or data-theme="dark"
on<html>. To load an alternate theme, add its stylesheet:
<link rel="stylesheet" href="/static/themes/holy-light.css">
Scaffolding
When chirp-ui is installed,chirp new <name>scaffolds a project with
use_chirp_ui(app) already wired into the app module and chirpui.csslinked
in the base template.
Next steps
- RAG Demo — uses chirp-ui for layout, cards, badges, and alerts.
- App Shell guide — build a persistent sidebar/topbar app.
- Islands — chirp-ui's
island_rootand state primitives for high-state widgets. - chirp-ui on GitHub — full component reference, showcase app, and development docs.