# chirp-ui URL: /docs/guides/chirp-ui/ Section: guides Tags: guides, chirp-ui, components, kida, htmx -------------------------------------------------------------------------------- Overview chirp-ui is a component library for Chirp. It provides Kida template macros — cards, modals, forms, layouts — that render as HTML. Use them with htmx for swaps, SSE for streaming, and View Transitions for polish. Zero JavaScript for layout. What's good about it: Gorgeous by default — Full visual design out of the box. Override --chirpui-* CSS variables to customize. htmx-native — Interactive components use htmx or native HTML (<dialog>, <details>). No client-side framework. Composable — {% slot %} for content injection. Components nest freely. Modern CSS — :has(), container queries, fluid typography, prefers-color-scheme dark mode. Installation Requires Python 3.14+. chirp extra standalone pip install bengal-chirp[ui] # or uv add "bengal-chirp[ui]" pip install chirp-ui # or uv add chirp-ui Setup Two steps to wire chirp-ui into your app: 1. Wire chirp-ui into your app Use Chirp's integration helper to serve chirpui.css, themes, transitions, and register required filters: from chirp import App, AppConfig, use_chirp_ui app = App(AppConfig(template_dir="templates")) use_chirp_ui(app) Import: use_chirp_ui is provided by Chirp. Use from chirp import use_chirp_ui when the chirp[ui] extra is installed. If that fails (e.g. older Chirp), use from chirp.ext.chirp_ui import use_chirp_ui. use_chirp_ui(app) adds StaticFiles middleware for the chirp-ui package directory (default /static) and registers filters (bem, field_errors, html_attrs, validate_variant) so chirp-ui components render correctly. 2. Include CSS in your base template <link rel="stylesheet" href="/static/chirpui.css"> For View Transitions support, add: <link rel="stylesheet" href="/static/chirpui-transitions.css"> Auto-detection When chirp-ui is installed, Chirp's template loader adds the chirp-ui package automatically. No configuration needed for {% from "chirpui/..." %} imports. Templates resolve chirpui/layout.html, chirpui/card.html, etc. from the package. Quick example {% 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 Quick start: Extend chirpui/app_shell_layout.html and fill the blocks. No manual HTML boilerplate: {# 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 %} Adding an inner shell: For nested layouts (e.g. forum > subforum), use the shell_section macro from Chirp: {% from "chirp/macros/shell.html" import shell_section %} {% call shell_section("forum-content") %} {% block content %}{% end %} {% end %} Migrating from boost.html: Replace {% extends "chirp/layouts/boost.html" %} with {% extends "chirpui/app_shell_layout.html" %}. Add {% block brand %}, {% block sidebar %}, etc. The hx-select="#page-content" and id="page-content" are already in place. Page spacing contract: Let the page-level wrapper own vertical rhythm. A good pattern is a parent layout with a page_root block that contains container() + stack(gap="lg"), while inner blocks such as page_content hold the page-specific sections. Pair that with Page(..., "page_content", page_block_name="page_root", ...) or PageComposition(..., fragment_block="page_content", page_block="page_root", ...) so boosted navigation swaps the full page shell instead of a too-narrow inner fragment. Manual shell: For full control, chirp-ui provides components for building persistent dashboard shells: sidebar, breadcrumbs, and command_palette. Combine them in a standalone _layout.html: {# 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" hx-boost="true" hx-target="#main" hx-swap="innerHTML transition:true" hx-select="#page-content"> <div id="page-content"> {% block content %}{% end %} </div> </main> </div> {{ command_palette(search_url="/search") }} {{ toast_container() }} </body> </html> Why standalone? Chirp's render_with_blocks({"content": ...}) replaces {% block content %} entirely. If you extend boost.html and put the shell inside {% block content %}, it gets overwritten. A standalone layout puts the shell outside the content block so it always renders. See Filesystem Routing for the full explanation. Why hx-select? On htmx-boosted navigation, Chirp returns a full HTML page (it renders the matched layout). Without hx-select, htmx would swap the entire response into #main, replacing the shell. hx-select="#page-content" tells htmx to parse the response and extract only #page-content — the shell stays untouched. Why no hx-disinherit? Boosted links inside the content area need to inherit hx-target="#main", hx-swap, and hx-select from the <main> element. If you add hx-disinherit, boosted links fall back to targeting body, which replaces everything. Fragment requests with explicit hx-target (e.g. hx-target="#compare-result") override the inherited value naturally. Component categories Category Examples 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, use these patterns for consistent structure: Section with header actions — Put section-level buttons (Refresh, Auto-detect, Run validation) in the section actions 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="dori setup cursor") }} {{ settings_row("Skills directory", status="ok", detail="/path/to/skills") }} {% end %} When to use what — Use description_list for term + detail only (no status badge). Use settings_row_list when you have label + status + detail. Theming chirp-ui uses prefers-color-scheme for dark mode. Override any --chirpui-* variable: :root { --chirpui-accent: #7c3aed; --chirpui-container-max: 80rem; } For manual light/dark toggle, set data-theme="light" or data-theme="dark" on <html>. Optional theme: <link rel="stylesheet" href="/static/themes/holy-light.css"> chirp new When chirp-ui is installed, chirp new <name> scaffolds a project with use_chirp_ui(app) wired in the app module. The base template includes chirpui.css. Next steps chirp-ui on GitHub — Full component reference, showcase app, and development docs RAG Demo — Uses chirp-ui for layout, cards, badges, and alert Islands Contract — chirp-ui provides island_root and state primitives for high-state widgets -------------------------------------------------------------------------------- Metadata: - Word Count: 1121 - Reading Time: 6 minutes