Alpine.js

Local UI state with Alpine.js — dropdowns, modals, tabs

2 min read 468 words

Overview

Alpine.js complements htmx for client-only UI state: dropdowns, modals, tabs, accordions. htmx handles server round-trips; Alpine handles interactions that don't need a request.

Chirp integrates Alpine via config and macros. When enabled, the Alpine script is auto-injected before</body>.

When to Use Alpine vs htmx

Use Alpine for Use htmx for
Dropdowns, modals, tabs Form submissions, partial swaps
Toggles, accordions Search-as-you-type
Local validation before submit SSE live updates
Client-only state Server-driven content

Enabling Alpine

from chirp import App, AppConfig

config = AppConfig(alpine=True)
app = App(config=config)

The Alpine script is injected into full-page HTML responses only. Fragment responses (htmx partials) are unchanged.

Configuration Options

Field Type Default Description
alpine bool False Enable Alpine.js script injection
alpine_version str "3.15.8" Pinned Alpine version (unpkg CDN)
alpine_csp bool False Use CSP-safe build for strict Content-Security-Policy

For strict CSP, setalpine_csp=Trueand ensure your CSP allows the Alpine script.

Using the Macros

Import Chirp's Alpine macros and use them in your templates:

{% from "chirp/alpine.html" import dropdown, modal, tabs %}

{% call dropdown("Menu") %}
  <a href="/a">Link A</a>
  <a href="/b">Link B</a>
{% end %}

{% call modal("confirm-dialog", title="Confirm") %}
  <p>Are you sure?</p>
  <button @click="open = false">Yes</button>
  <button @click="open = false">Cancel</button>
{% end %}

{% call tabs(["Overview", "Details"], "Overview") %}
  <div x-show="active === 'Overview'">Overview content</div>
  <div x-show="active === 'Details'">Details content</div>
{% end %}
  • dropdown(trigger, wrapper_class="", panel_class="")— Toggle panel with click-outside and Escape key
  • Accessible:aria-expanded, aria-haspopup, role="menu"
  • modal(id, title="", wrapper_class="", content_class="", managed=true)— Dialog with Escape to close
  • managed=true (default): self-contained. managed=false: use parent's openvariable so a sibling button can control it
  • Add[x-cloak]{display:none!important}to your CSS so the modal stays hidden until Alpine initializes
  • Accessible:role="dialog", aria-modal, aria-hidden

Tabs

  • tabs(tab_names, default=none, tab_list_class="", panel_class="")— Tab list + panel slot
  • Caller provides panel content withx-show="active === 'TabName'"per panel

htmx + Alpine Together

Alpine 3 uses a mutation observer to discover new elements. When htmx swaps in HTML that contains Alpine attributes (x-data, x-show), Alpine initializes them automatically. No extra wiring needed.

Example: a dropdown inside an htmx-swapped fragment:

<div id="user-card" hx-get="/users/1" hx-trigger="load" hx-swap="innerHTML">
  Loading...
</div>

The server returns:

{% from "chirp/alpine.html" import dropdown %}
{% call dropdown("Actions") %}
  <a href="/users/1/edit">Edit</a>
  <button hx-delete="/users/1" hx-target="#user-card">Delete</button>
{% end %}

Alpine initializes the dropdown when the fragment is swapped in.

CSP Setup

For strict Content-Security-Policy:

  1. SetAppConfig(alpine_csp=True)
  2. Ensure your CSP allows the Alpine script source (e.g.https://unpkg.com)
  3. If usingeval()-based policies, Alpine's CSP build avoids eval