# Complete Alpine.js + htmx Onboarding Package

**Verified:** 2026-07-19  
**Baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)

This combined edition contains the same twelve standalone lessons included in the package directory.

## Table of contents

- [Day 1: Start Here — Ownership, Boundaries, and the Web-Native Mental Model](#day-1-start-here--ownership-boundaries-and-the-web-native-mental-model)
- [Day 2: Alpine Local-UI Fundamentals](#day-2-alpine-local-ui-fundamentals)
- [Day 3: htmx Request, Trigger, Target, and Swap Fundamentals](#day-3-htmx-request-trigger-target-and-swap-fundamentals)
- [Day 4: Forms, Native Controls, Server Validation, and Local Previews](#day-4-forms-native-controls-server-validation-and-local-previews)
- [Day 5: Request State, Concurrency, Active Search, and Race-Safe Operations](#day-5-request-state-concurrency-active-search-and-race-safe-operations)
- [Day 6: Component Lifecycle, Dynamic Activation, Cleanup, Preservation, and Morphing](#day-6-component-lifecycle-dynamic-activation-cleanup-preservation-and-morphing)
- [Day 7: Semantic Events and Cross-Boundary Contracts](#day-7-semantic-events-and-cross-boundary-contracts)
- [Day 8: History, Navigation, Search Routes, and Progressive Enhancement](#day-8-history-navigation-search-routes-and-progressive-enhancement)
- [Day 9: Accessible Modals, Drawers, Remote Forms, and Inline Editing](#day-9-accessible-modals-drawers-remote-forms-and-inline-editing)
- [Day 10: Out-of-Band Updates, Toasts, Notifications, Jobs, and Live Panels](#day-10-out-of-band-updates-toasts-notifications-jobs-and-live-panels)
- [Day 11: Production Security, Error Doctrine, Testing, Naming, and Daily Operations](#day-11-production-security-error-doctrine-testing-naming-and-daily-operations)
- [Day 12: Power-User Showcase — Pragmatic Systems That Feel Slightly Impossible](#day-12-power-user-showcase--pragmatic-systems-that-feel-slightly-impossible)

---

# Day 1: Start Here — Ownership, Boundaries, and the Web-Native Mental Model

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Establish the architectural rules that prevent Alpine and htmx from becoming competing frameworks. This is the startup primer: installation, vocabulary, decision rules, and the smallest useful examples.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

By the end of the day, you should be able to:

- explain what Alpine, htmx, the server, HTML, CSS, URLs, native controls, and plain JavaScript each own;
- classify a feature as Alpine-only, htmx-only, mixed, or a specialized JavaScript island;
- draw a replacement boundary before writing `hx-target`;
- decide whether local state should reset or survive a server update;
- recognize the most common duplicated-authority failures.

## 1. The operating model

The libraries are peers, not substitutes:

| Layer | Default responsibility |
| --- | --- |
| Server and database | Durable truth, authorization, validation, transactions, workflow state |
| Server templates | Canonical HTML representations of server truth |
| URL and routes | Navigable, shareable, refresh-safe locations |
| HTML | Structure, native controls, forms, links, semantics, accessibility relationships |
| CSS | Layout, presentation, ordinary transitions, request-state styling |
| htmx | HTTP requests, trigger timing, request coordination, history integration, targeted swaps |
| Alpine | Immediate browser-local state such as open/closed, selected, focused, provisional, or dismissed |
| Plain JavaScript | Browser APIs, nontrivial algorithms, rich widgets, high-frequency rendering |
| Browser events | Contracts between independently owned regions |

The team convention is:

> Share a workflow, but do not share authority over the same state value or the same DOM descendants.

This is an architectural convention, not a library limitation. Both libraries are technically capable of doing more. The convention keeps the system understandable under refresh, failure, history restoration, and repeated swaps.

## 2. Two constitutional rules

### Rule A: one authoritative owner per state value

A value may have drafts, projections, or cached representations, but one place must decide what is true.

Example: a cart line quantity.

- The native input contains the proposed quantity.
- Alpine may display a provisional subtotal.
- htmx submits the input.
- The server checks inventory, price, discounts, tax, and authorization.
- The returned HTML is the canonical cart representation.

The Alpine subtotal is a preview. It is not a second cart engine.

Use this ownership table:

| Meaning or lifetime | Default owner |
| --- | --- |
| Durable, shared, financial, permission-sensitive | Server |
| Bookmarkable or meaningful after refresh | URL and route |
| Required in a request | Named native control or an explicit htmx parameter |
| Exists for one local interaction | Alpine |
| Exists only while a request is in flight | htmx lifecycle plus CSS |
| Browser-only preference | Browser storage, optionally surfaced through Alpine |
| Complex rich-widget internals | Plain JavaScript island |

Use the refresh test:

> If losing the value on refresh would make the application incorrect or conceal real state, Alpine must not be its only authority.

### Rule B: one intentional writer per DOM boundary

For every meaningful region, choose one current writer:

1. htmx-owned and replaceable;
2. Alpine-owned and stable;
3. a stable Alpine shell with an htmx-owned interior;
4. an htmx-replaceable component containing disposable Alpine behavior;
5. an isolated JavaScript island;
6. static or native HTML requiring neither library.

The most damaging integration bugs are usually boundary bugs, not syntax bugs.

## 3. Installation SOP

A production project should use pinned or self-hosted assets according to its dependency policy. A minimal local layout is:

```html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Alpine + htmx Training</title>

    <style>
        [x-cloak] { display: none !important; }
        .htmx-indicator { display: none; }
        .htmx-request .htmx-indicator,
        .htmx-request.htmx-indicator { display: inline; }
    </style>

    <script src="/vendor/htmx-2.0.10.min.js" defer></script>
    <script src="/vendor/alpine-3.15.12.min.js" defer></script>
</head>
<body>
    <main>
        <!-- exercises -->
    </main>
</body>
</html>
```

Operational rules:

1. Pin versions; do not depend on an unversioned CDN URL in production.
2. Use `defer` when loading the script-tag builds.
3. Add `[x-cloak]` before writing initially hidden Alpine UI.
4. Confirm the server can return complete pages and target-specific fragments.
5. Record the htmx major version in the repository because htmx 4 changes defaults and event names.
6. Load Alpine plugins before Alpine initializes, following the plugin’s official installation instructions.
7. Run Alpine initialization only once when using a bundled module build.

## 4. Smallest Alpine-only operation

Use Alpine when the interaction is immediate, local, and safe to discard.

```html
<section x-data="{ open: false }">
    <button
        type="button"
        @click="open = !open"
        :aria-expanded="open"
        aria-controls="local-details"
    >
        Toggle details
    </button>

    <div id="local-details" x-cloak x-show="open">
        This content is already in the document.
    </div>
</section>
```

Ownership:

- `open`: Alpine;
- button and relationship: HTML;
- visibility styling: Alpine state reflected through DOM/CSS;
- durable state: none.

A request would add latency and server load without adding authority.

## 5. Smallest htmx-only operation

Use htmx when the browser needs a server representation.

```html
<a
    href="/customers/42"
    hx-get="/customers/42/card"
    hx-target="#customer-panel"
    hx-swap="innerHTML"
>
    Load customer
</a>

<section id="customer-panel" aria-live="polite"></section>
```

Expected fragment:

```html
<article>
    <h2>Ada Lovelace</h2>
    <p>Customer since 2024</p>
</article>
```

Ownership:

- customer truth: server;
- representation: server template;
- request and installation: htmx;
- target contents: htmx;
- non-enhanced route: the real `href`.

The target should be the smallest coherent server-owned region.

## 6. Preferred mixed shape: stable shell, replaceable interior

Use this when local presentation must survive server refreshes.

```html
<aside
    x-data="{ open: false }"
    @cart-updated.window="open = true"
>
    <button
        type="button"
        @click="open = !open"
        :aria-expanded="open"
        aria-controls="cart-drawer"
    >
        Cart
    </button>

    <section id="cart-drawer" x-cloak x-show="open" x-transition>
        <div id="cart-contents">
            <!-- server-rendered cart -->
        </div>
    </section>
</aside>

<button
    type="button"
    hx-post="/cart/items/42"
    hx-target="#cart-contents"
    hx-swap="outerHTML"
>
    Add item
</button>
```

The response must return an element whose root matches the chosen swap:

```html
<div id="cart-contents">
    <!-- canonical updated cart -->
</div>
```

The outer `<aside>` is not targeted. Therefore its Alpine state survives. The server may emit a semantic `cart-updated` event after success; event mechanics are covered on Day 7.

## 7. Correct disposable shape

Use a replaceable Alpine root when reset is desirable.

```html
<article id="customer-42" x-data="{ menuOpen: false }">
    <button type="button" @click="menuOpen = !menuOpen">
        Actions
    </button>

    <div x-cloak x-show="menuOpen">
        Temporary row actions
    </div>

    <button
        type="button"
        hx-get="/customers/42/edit"
        hx-target="#customer-42"
        hx-swap="outerHTML"
    >
        Edit
    </button>
</article>
```

The returned editor replaces the entire article. Losing `menuOpen` is correct because that state described the old representation.

## 8. Decision SOP for every new interaction

Before adding either library, execute this sequence:

1. **Can semantic HTML do it?** Use a link, form, disclosure pattern, `<dialog>`, native validation, or ordinary control where practical.
2. **Can CSS do it without application state?** Prefer CSS for layout, responsive behavior, hover, focus, and ordinary transitions.
3. **Is this a specialized browser algorithm?** Use a small JavaScript module or isolated widget.
4. **Must the server be consulted or durable state change?** Use htmx.
5. **Is it immediate, temporary, and local?** Use Alpine.
6. **Does it need both?** Separate the Alpine shell from the htmx target and connect them through controls or semantic events.
7. **Do both appear to write the same descendants or own the same value?** Stop and redesign.

Write the answer in a component contract before implementation.

## 9. First-day component contract

For a simple feature, record:

```text
Feature:
Durable state owner:
URL state owner:
Submitted values:
Alpine-local values:
htmx trigger:
htmx target:
Swap strategy:
Should Alpine state reset or survive?
Focus behavior:
Failure representation:
```

Example:

```text
Feature: customer edit drawer
Durable state owner: server/database
URL state owner: none
Submitted values: named form controls
Alpine-local values: drawer open/closed
htmx trigger: click Edit; form submit
htmx target: #customer-editor-body
Swap strategy: innerHTML for load; outerHTML for form
Should Alpine state reset or survive? drawer survives validation swaps
Focus behavior: focus first field after content-loaded event; restore trigger on close
Failure representation: full server-rendered form with values and errors
```

## 10. Anti-pattern recognition drill

### Shadow backend

```html
<div x-data="{ users: [], permissions: {}, cart: {} }">
```

This is suspicious when the values mirror durable server entities and business rules.

### Request for local trivia

```html
<button hx-get="/open-menu">Open menu</button>
```

A menu that already exists locally should not require a request.

### Two writers

```html
<ul id="orders" x-data="{ orders: [] }">
    <template x-for="order in orders">...</template>
</ul>

<button hx-get="/orders" hx-target="#orders">Refresh</button>
```

Alpine and htmx both claim the collection. Choose one writer.

### Broad destructive target

```html
<body x-data="{ sidebarOpen: true }">
    <button hx-get="/report" hx-target="body">Load report</button>
</body>
```

This destroys unrelated state and complicates focus, history, and initialization. Target a report region or intentionally perform navigation.

## 11. Day lab: classify and repair

Create a page containing these four features:

1. a local FAQ disclosure;
2. a server-loaded customer card;
3. a cart drawer whose contents update without closing;
4. an inline editor whose menu state resets after the row is replaced.

For each feature:

- label the Alpine root with an HTML comment;
- label the htmx target with an HTML comment;
- write the state owner beside each state value;
- explain in one sentence why refresh is safe or unsafe;
- test the intended reset/survival behavior by manually replacing the target in DevTools.

## Mini-tasks

### Task A: ownership sorting

Assign an owner to each value:

- selected density preference;
- applied search query;
- typed but unsubmitted note;
- current invoice balance;
- whether a dropdown is open;
- whether a save request is in flight;
- drag ghost coordinates;
- current authenticated role.

### Task B: boundary repair

A modal root contains `x-data="{ open: true }"` and is also the `hx-target` for every validation response. Change the markup so the shell survives while the form is replaced.

### Task C: progressive baseline

Turn an htmx-only button into a real link or form that still works when JavaScript is unavailable.

## Comprehension check

Answer without looking back:

1. What does `x-data` define?
2. What does `hx-target` define beyond “where to put HTML”?
3. When is replacing an Alpine root correct?
4. Why should server-required values live in native controls?
5. Who owns a request’s factual in-flight state?
6. What is the refresh test?
7. Name the two preferred mixed component shapes.
8. Why is a global Alpine store not the default communication mechanism?
9. What must a server return for an `outerHTML` swap?
10. What is the first repair when preservation or morphing seems necessary?

## Cheat sheet

```text
LOCAL + TEMPORARY                  -> Alpine
SERVER / DURABLE / SHARED         -> server reached through htmx
BOOKMARKABLE                      -> URL
SUBMITTED                         -> named control
REQUEST IN FLIGHT                 -> htmx lifecycle + CSS
SPECIALIZED ALGORITHM             -> plain JS island

STATE MUST SURVIVE SWAP            -> Alpine root outside target
STATE SHOULD RESET                -> replace Alpine root
BOTH WRITE SAME DESCENDANTS       -> redesign
```

## Exit exercise

Design a “notification center” on paper. The server owns notification records and unread count. The panel may stay open while records refresh. A toast may appear after a new notification arrives. Identify every owner, target, event, and reset/survival decision. Do not write code until the contract has no duplicated authority.

## Exit criteria

You pass Day 1 when you can review an unfamiliar component and identify:

- every durable, URL, control, request, and local state value;
- the mutation owner of each region;
- whether each Alpine root should reset or survive;
- whether the non-enhanced path is meaningful.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 2: Alpine Local-UI Fundamentals

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Learn the routine, single-purpose Alpine operations used in ordinary server-rendered applications, with emphasis on small state, native controls, accessibility, and safe interaction beside htmx.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- create a small `x-data` component and keep its boundary obvious;
- use `x-on`, `x-show`, `x-if`, `x-bind`, `x-text`, `x-model`, `x-ref`, `$nextTick`, and `$watch` appropriately;
- decide when to extract `Alpine.data()`;
- avoid turning Alpine into a router, network layer, database cache, or duplicate renderer;
- implement disclosures, counters, previews, tabs, and local preferences.

## 1. Alpine’s job

An Alpine component is a DOM subtree with reactive browser-local state.

```html
<div x-data="{ open: false }">
    <button type="button" @click="open = !open">Toggle</button>
    <section x-cloak x-show="open">Local content</section>
</div>
```

Read it as:

1. `x-data` starts a state scope.
2. `open` is safe to discard.
3. `@click` changes it immediately.
4. `x-show` reflects it without fetching anything.

Good default Alpine state:

- disclosure, dropdown, modal, or drawer visibility;
- active keyboard index;
- selected already-present tab;
- draft text and character count;
- provisional calculations;
- temporary client-side selection;
- toast presentation;
- browser-only preferences.

Poor default Alpine state:

- permissions;
- prices, inventory, balances, or workflow truth;
- normalized server entities;
- server validation results;
- application routing;
- an independent request cache.

## 2. `x-data`: keep local state local

Inline state is ideal when the component remains small:

```html
<section x-data="{ expanded: false }">
    <button type="button" @click="expanded = !expanded">
        Show policy
    </button>
    <div x-cloak x-show="expanded">
        Policy details
    </div>
</section>
```

Extract a registered component when you need reuse, methods, cleanup, or a readable template:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('disclosure', () => ({
        open: false,

        toggle() {
            this.open = !this.open
        },

        close() {
            this.open = false
        }
    }))
})
</script>

<section x-data="disclosure">
    <button type="button" @click="toggle" :aria-expanded="open">
        Toggle
    </button>
    <div x-cloak x-show="open">Details</div>
</section>
```

Team rule: attributes declare behavior; they should not contain an entire application implementation.

## 3. `x-on` and event modifiers

Use `@event` for browser-local interaction.

```html
<div x-data="{ open: false }">
    <button type="button" @click="open = true">Open menu</button>

    <div
        x-cloak
        x-show="open"
        @keydown.escape.window="open = false"
        @click.outside="open = false"
    >
        Menu content
    </div>
</div>
```

Useful modifiers:

| Modifier | Use |
| --- | --- |
| `.window` | listen at the window boundary |
| `.outside` | react to an outside click |
| `.prevent` | prevent the native default |
| `.stop` | stop propagation deliberately |
| `.escape`, `.enter`, etc. | keyboard-specific behavior |
| `.debounce` / `.throttle` | local event-rate control |

Do not use Alpine event handlers to recreate htmx request queues, target selection, swap logic, or history.

## 4. `x-show` versus `x-if`

### Use `x-show` when the subtree should remain mounted

```html
<div x-data="{ open: false }">
    <button type="button" @click="open = !open">Filters</button>
    <fieldset x-cloak x-show="open">
        <input name="q">
    </fieldset>
</div>
```

Advantages:

- child input values remain;
- focusable elements can be reused;
- transitions are supported;
- previously processed `hx-*` attributes remain on existing nodes.

### Use `x-if` when the subtree should truly cease to exist

```html
<div x-data="{ mounted: false }">
    <button type="button" @click="mounted = !mounted">
        Toggle editor
    </button>

    <template x-if="mounted">
        <section>
            A fresh subtree is created each time.
        </section>
    </template>
</div>
```

Important facts:

- `x-if` belongs on a `<template>`;
- the template contains one root element;
- local state in the removed subtree is destroyed;
- `x-transition` is not directly supported for `x-if`;
- if Alpine creates nodes containing `hx-*`, htmx may need `htmx.process()` on that new region. That integration is covered on Day 6.

## 5. `x-bind`: reflect state in attributes

```html
<div x-data="{ open: false }">
    <button
        type="button"
        @click="open = !open"
        :aria-expanded="open"
        :class="{ 'is-active': open }"
    >
        Toggle
    </button>
</div>
```

Good bindings:

- `aria-expanded`, `aria-selected`, `aria-pressed`;
- local classes;
- locally determined `disabled`;
- local `data-*` state;
- IDs generated for local relationships.

Do not bind authorization or prices from a stale browser copy.

## 6. `x-text` and `x-html`

Use `x-text` for safe local text output:

```html
<div x-data="{ note: '' }">
    <label>
        Note
        <textarea name="note" x-model="note"></textarea>
    </label>
    <p><span x-text="note.length"></span> characters</p>
</div>
```

Use `x-html` only for deliberately trusted HTML. In a server-rendered htmx application, the usual way to install server UI is to request server-rendered HTML through htmx, not to fetch a string and feed it to `x-html`.

Never pass unsanitized user content to `x-html`.

## 7. `x-model`: the strongest native bridge

Bind Alpine to a real control rather than maintaining a disconnected property.

```html
<form
    x-data="{ quantity: 1, unitPrice: 18 }"
    action="/cart/lines/42"
    method="post"
>
    <label>
        Quantity
        <input
            type="number"
            name="quantity"
            min="1"
            x-model.number="quantity"
        >
    </label>

    <output>
        Draft subtotal:
        $<span x-text="(quantity * unitPrice).toFixed(2)"></span>
    </output>

    <button type="submit">Update</button>
</form>
```

The native control remains the submission model. Alpine observes it for immediate feedback. The server still decides the accepted quantity and total.

Useful modifiers include `.number`, `.boolean`, `.lazy`, `.blur`, `.debounce`, and `.throttle`. Select a modifier for local reactivity. Let htmx own network debounce and cancellation when a server request is involved.

## 8. Methods, getters, effects, and watchers

Prefer a getter for pure derived state:

```html
<div x-data="{
    price: 12,
    quantity: 1,
    get subtotal() {
        return this.price * this.quantity
    }
}">
    <input type="number" min="1" x-model.number="quantity">
    <output x-text="subtotal.toFixed(2)"></output>
</div>
```

Use a method for an action:

```html
<div x-data="{
    items: [],
    add(label) {
        this.items.push({ id: crypto.randomUUID(), label })
    }
}">
```

Use `x-effect` for a local side effect that should rerun with its dependencies. Keep it narrow. Do not make effects a hidden network layer.

Use `$watch` when old and new values matter:

```html
<div
    x-data="{ density: 'comfortable' }"
    x-init="$watch('density', (value, oldValue) => {
        console.debug({ value, oldValue })
    })"
>
```

Avoid changing the watched property in a way that creates a loop.

## 9. DOM references and Alpine timing

Use `x-ref` for scoped DOM access:

```html
<div x-data>
    <input x-ref="search">
    <button type="button" @click="$refs.search.focus()">
        Focus search
    </button>
</div>
```

Use `$nextTick` after Alpine changes its own DOM:

```html
<div x-data="{ open: false }">
    <button
        type="button"
        @click="open = true; $nextTick(() => $refs.panel.focus())"
    >
        Open and focus
    </button>

    <section
        x-cloak
        x-show="open"
        x-ref="panel"
        tabindex="-1"
    >
        Panel
    </section>
</div>
```

`$nextTick` means Alpine finished its reactive update. It does not mean an htmx response has arrived or settled.

## 10. `x-for`: small browser-owned collections only

```html
<div x-data="{ tags: ['urgent', 'review'] }">
    <template x-for="tag in tags" :key="tag">
        <span x-text="tag"></span>
    </template>
</div>
```

Appropriate:

- draft tags;
- temporary choices;
- a local toast queue;
- a small local preview.

Suspicious:

- hundreds of database records;
- lists also replaced by htmx;
- records whose truth lives on the server.

Use stable keys for reorderable items.

## 11. Alpine stores: an exception, not the starting point

A store is shared reactive browser state.

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.store('ui', {
        sidebarOpen: false,
        toasts: []
    })
})
</script>
```

Reasonable contents:

- theme;
- global command-palette visibility;
- toast queue and timers;
- small non-authoritative preferences.

Do not put routing, permissions, cart truth, or a server entity cache there by default.

## 12. Accessibility SOP for local components

For a disclosure:

1. use a real button;
2. bind `aria-expanded`;
3. add `aria-controls` where useful;
4. keep keyboard behavior native;
5. avoid click-only non-button elements;
6. use `x-cloak` for initially hidden content.

For a custom tab interface:

1. use correct tab roles only when implementing the full keyboard model;
2. separate focused tab from selected tab if activation is manual;
3. generate stable local IDs with `x-id`/`$id` if needed;
4. do not use Alpine tabs for meaningful server routes that belong in the URL.

For an overlay:

1. prefer native `<dialog>` or a well-tested focus implementation;
2. restore focus to the opener;
3. support Escape;
4. keep labels and descriptions explicit;
5. do not make the whole `body` inert with a simplistic Alpine effect that also disables the dialog.

## 13. Day lab: local interaction toolkit

Build a single page with:

1. an FAQ disclosure;
2. a character counter using a named `<textarea>`;
3. already-present tabs whose selection is disposable;
4. a provisional invoice-line subtotal;
5. a local density preference;
6. a temporary tag list rendered by `x-for`.

For each component:

- keep the `x-data` root as small as possible;
- use no server request;
- write a one-line explanation of why refresh is safe;
- add keyboard and accessibility behavior;
- decide whether inline state is still readable or should become `Alpine.data()`.

## Mini-tasks

### Task A: choose `x-show` or `x-if`

Select one and explain why:

- a repeatedly opened dropdown;
- an expensive editor that should reset completely;
- a filter fieldset whose typed values must remain while hidden;
- a temporary preview that must release a ResizeObserver on close.

### Task B: remove duplicated form state

Repair a form that keeps `email` in an Alpine store, a separate `x-data` property, and the input value.

### Task C: extract a component

Turn a 12-line inline object with `open`, `toggle()`, Escape handling, and cleanup into `Alpine.data('popover', ...)`.

## Comprehension check

1. When is `x-show` better than `x-if`?
2. What does `.number` change on `x-model`?
3. Why is `x-text` generally safer than `x-html`?
4. What does `$nextTick` guarantee?
5. What should `x-for` normally render in an htmx application?
6. When is a global store justified?
7. Why should a server-required value appear in a named control?
8. What is wrong with using `$watch` as an invisible HTTP client?
9. Name two cases where plain JavaScript is more proportionate than Alpine.
10. What must happen to external resources when an Alpine component is destroyed?

## Cheat sheet

```text
x-data       local component scope
@click       local browser event
x-show       hide/show, node remains
x-if         create/destroy a fresh subtree
:attr        reflect local state in an attribute
x-text       safe local text
x-model      bind local state to a real control
x-for        small browser-owned collection
x-ref        scoped DOM reference
$nextTick    after Alpine's DOM update
$dispatch    semantic browser event
Alpine.data  reusable/extracted component
Alpine.store genuinely shared browser UI state
```

## Exit exercise

Implement a “bulk-selection toolbar” for rows already present in the page. Alpine may own the temporary selected IDs and toolbar visibility. Do not perform a server request yet. Include a hidden named control or a documented future serialization plan so Day 4 can submit the selection without creating a private, undocumented store protocol.

## Exit criteria

You pass when your Alpine components are:

- small and local;
- safe to discard unless explicitly persisted as a preference;
- based on native controls and semantic HTML;
- free of duplicated server truth;
- ready to coexist with a separately owned htmx target.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 3: htmx Request, Trigger, Target, and Swap Fundamentals

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Learn the ordinary htmx operations a developer performs every day: making requests, choosing triggers, declaring a small target, matching the response root to the swap, and preserving a valid non-enhanced path.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- use `hx-get`, `hx-post`, `hx-put`, `hx-patch`, and `hx-delete` with appropriate server semantics;
- select triggers, targets, and swap strategies intentionally;
- return HTML fragments whose root matches the target contract;
- keep server authority visible;
- avoid accidental client rendering and broad destructive swaps.

## 1. The htmx mental model

htmx lets an element describe an HTTP interaction in HTML.

```html
<button
    type="button"
    hx-get="/customers/42/card"
    hx-target="#customer-panel"
    hx-swap="innerHTML"
>
    Load customer
</button>

<section id="customer-panel"></section>
```

Read it as:

1. `hx-get` chooses the request.
2. The button’s default trigger is `click`.
3. `hx-target` declares the server-owned replacement boundary.
4. `hx-swap` declares how response HTML enters the target.

The server normally returns HTML:

```html
<article>
    <h2>Ada Lovelace</h2>
    <p>Customer since 2024</p>
</article>
```

htmx is not a JSON-to-component rendering loop. URLs, forms, HTTP, server templates, and HTML remain central.

## 2. Request-method SOP

### GET: retrieve a representation

```html
<a
    href="/orders?page=2"
    hx-get="/orders?page=2"
    hx-target="#orders"
    hx-push-url="true"
>
    Next page
</a>
```

GET should not make a durable mutation. A real `href` keeps navigation meaningful.

### POST: create or submit a command

```html
<form
    action="/customers"
    method="post"
    hx-post="/customers"
    hx-target="#customer-form"
    hx-swap="outerHTML"
>
    <!-- named controls -->
</form>
```

### PUT/PATCH: update according to server conventions

HTML forms natively submit GET or POST. `hx-put` and `hx-patch` are useful enhanced interactions, but the server must authorize and validate them. Define the non-enhanced fallback according to the project’s routing conventions.

### DELETE: delete an authorized resource

```html
<button
    type="button"
    hx-delete="/notifications/42"
    hx-target="closest li"
    hx-swap="delete"
>
    Dismiss
</button>
```

Return a successful response appropriate to the project. Under htmx 2.x, a `204` causes no swap; when relying on a `delete` swap, use a tested successful response policy such as `200`.

## 3. Targets are lifecycle contracts

A target is not merely a selector convenience.

Before choosing one, ask:

- Does it contain an Alpine root?
- Should that local state survive?
- Does it contain focused input?
- Does it contain a third-party widget?
- Is it larger than the returned representation?
- Does the response contain the correct root for the swap?
- Is the target stable under repeated requests?
- Could an out-of-band response also address it by ID?

Target the smallest coherent server-owned component.

### Good

```html
<section x-data="{ open: true }">
    <div id="report-results">
        <!-- htmx-owned -->
    </div>
</section>
```

`#report-results` can be replaced without destroying the stable Alpine shell.

### Suspicious

```html
<main id="app" x-data="{ sidebarOpen: true, modalOpen: false }">
    ...
</main>

<button hx-get="/report" hx-target="#app">Refresh report</button>
```

A report refresh should not destroy unrelated shell state.

## 4. Swap strategy SOP

| Strategy | Meaning | Typical use |
| --- | --- | --- |
| `innerHTML` | keep target root; replace children | stable server panel shell |
| `outerHTML` | replace target root | complete server component/form/row |
| `beforeend` | append response | load more, feed page |
| `afterbegin` | prepend response | newest item at top |
| `delete` | remove target after successful request | authorized deletion |
| `none` | no primary swap | response events or OOB-only response |

### `innerHTML`

```html
<section id="customer-panel"></section>
```

Response:

```html
<article>...</article>
```

### `outerHTML`

```html
<article id="customer-42">...</article>
```

Response must include:

```html
<article id="customer-42">...</article>
```

A common failure is returning only the article’s children while requesting `outerHTML`.

### Append

```html
<ul id="activity-feed">
    <!-- existing rows -->
</ul>

<button
    hx-get="/activity?page=2"
    hx-target="#activity-feed"
    hx-swap="beforeend"
>
    Load more
</button>
```

The response should contain list items, not a second nested `<ul>` unless that is intentional.

## 5. Triggers

Default triggers depend on the element: click for many controls, submit for forms, change for some controls. Use `hx-trigger` when timing differs.

### Debounced input

```html
<input
    id="customer-query"
    type="search"
    name="q"
    hx-get="/customers/search"
    hx-trigger="input changed delay:300ms"
    hx-target="#customer-results"
    hx-sync="this:replace"
>
```

`changed` means the element’s value must differ. `delay` debounces. `hx-sync` handles an already in-flight request; Day 5 covers it in depth.

### Polling

```html
<div
    hx-get="/jobs/42/status"
    hx-trigger="every 2s"
    hx-target="this"
    hx-swap="innerHTML"
>
    Waiting for status…
</div>
```

The server remains authoritative for job state.

### Custom events

```html
<section
    hx-get="/report"
    hx-trigger="report-refresh-requested from:window"
    hx-target="this"
    hx-swap="outerHTML"
>
    Current report
</section>
```

Use semantic custom events. Day 7 defines the event contract.

### Scope caution

A `from:` selector is evaluated according to htmx trigger rules and may be broader than the visual component. Prefer explicit IDs, `window` for deliberate cross-component events, or attributes placed directly on the triggering control. Do not write `from:input` on a large page and assume it means “inputs inside this form.”

## 6. Parameters and native controls

Forms and named controls are the normal request model.

```html
<form
    action="/search"
    method="get"
    hx-get="/search"
    hx-target="#results"
>
    <input type="search" name="q">
    <select name="status">
        <option value="">All</option>
        <option value="open">Open</option>
    </select>
    <button type="submit">Search</button>
</form>
```

Additional tools, in preferred order:

1. real named controls;
2. `hx-include` for related controls outside the triggering element;
3. `hx-params` to include or exclude parameters;
4. `hx-vals` for small explicit supplemental values;
5. `hx-headers` for deliberate metadata such as a framework-approved CSRF header.

Avoid serializing a large Alpine store into `hx-vals`.

## 7. Response design

After a mutation, return the resulting authoritative representation rather than a bare success object.

Request:

```html
<form
    hx-post="/profile"
    hx-target="#profile-form"
    hx-swap="outerHTML"
>
```

Success response:

```html
<section id="profile-form">
    <p role="status">Profile saved.</p>
    <!-- canonical profile representation -->
</section>
```

Validation response:

```html
<form
    id="profile-form"
    action="/profile"
    method="post"
    hx-post="/profile"
    hx-target="#profile-form"
    hx-swap="outerHTML"
>
    <div role="alert">
        Correct the highlighted fields.
    </div>
    <!-- submitted values and field errors -->
</form>
```

Under htmx 2.x, `4xx` and `5xx` responses do not swap by default. A team must choose one tested policy:

- return validation HTML with a swap-eligible status such as `200`; or
- configure error-response swapping deliberately.

Do not depend on accidental defaults.

## 8. Progressive enhancement baseline

Prefer:

- real links with `href`;
- forms with `action` and `method`;
- meaningful routes;
- complete pages for direct visits;
- server-rendered errors;
- buttons only when there is no navigation fallback.

A route used with `hx-push-url` must return a complete page when opened directly.

If the same URL returns a full page for normal navigation and a fragment for htmx, configure HTTP caches correctly, commonly with `Vary: HX-Request` or an equivalent framework policy.

## 9. Request headers and server behavior

htmx sends request metadata such as `HX-Request` and `HX-Target`. These are hints for representation selection, not authorization credentials.

The server must still:

- authenticate;
- authorize every operation;
- validate all input;
- enforce CSRF policy;
- perform transactions;
- handle conflicts;
- escape untrusted output;
- return coherent HTML.

## 10. Day lab: customer directory

Build:

1. a real search GET form;
2. a paginated result region;
3. a load-customer detail panel;
4. an inline delete action for a notification;
5. an edit form that replaces its complete root.

For each interaction, write:

```text
Method:
Route:
Trigger:
Inputs:
Target:
Swap:
Expected response root:
Non-enhanced behavior:
Status-code policy:
```

Test each route directly without htmx.

## Mini-tasks

### Task A: match roots

Repair three `outerHTML` examples whose server response omits the target root.

### Task B: shrink targets

A refresh button targets the entire dashboard. Refactor it to target one coherent report card.

### Task C: replace JSON rendering

Convert an Alpine `fetch('/orders').then(r => r.json())` list renderer into an htmx request returning server HTML.

### Task D: trigger scope

Replace a broad `hx-trigger="input from:input"` with a component-safe trigger arrangement.

## Comprehension check

1. What four decisions are expressed by a basic htmx interaction?
2. Why is an `hx-target` a lifecycle contract?
3. When should `outerHTML` be used?
4. What must its response contain?
5. What is the difference between `delay` and `hx-sync`?
6. Why should GET not make durable changes?
7. What happens with a `204` response in htmx 2.x?
8. Why are real links and forms valuable even in an enhanced application?
9. When is `hx-include` preferable to `hx-vals`?
10. Why is `HX-Request` not an authorization mechanism?

## Cheat sheet

```text
hx-get/post/...  request
hx-trigger       when request starts
hx-target        server-owned replacement boundary
hx-swap          how response enters
hx-include       include related controls
hx-push-url      create navigable history state
hx-sync          coordinate overlapping requests
hx-indicator     request progress
hx-swap-oob      related authoritative update elsewhere
```

## Exit exercise

Implement a server-rendered “support tickets” page with search, pagination, details, and status update. Every pushed URL must work directly. No server list may be rebuilt with `x-for`. Alpine may be used only for a local filter disclosure and must survive result swaps.

## Exit criteria

You pass when every htmx interaction has:

- a meaningful route and method;
- declared inputs;
- the smallest coherent target;
- a response root that matches the swap;
- an explicit success/error policy;
- a valid direct or non-enhanced path where practical.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 4: Forms, Native Controls, Server Validation, and Local Previews

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Combine native forms, Alpine reactivity, htmx submission, and server-rendered validation without duplicating form architecture or business rules.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- use named native controls as the shared Alpine–htmx bridge;
- build conditional fields and provisional previews with Alpine;
- submit and replace forms through htmx;
- return accessible server-rendered validation;
- preserve values, focus, and a valid non-enhanced path;
- distinguish local hints from trusted validation.

## 1. Ownership map for a mixed form

| Concern | Owner |
| --- | --- |
| Labels, controls, names, grouping | HTML |
| Immediate native constraints | Browser |
| Local disclosure, hints, and previews | Alpine |
| Submission, target, swap, request state | htmx |
| Authorization and trusted validation | Server |
| Error and success markup | Server templates |
| Durable result | Server/database |
| Focus after replacement | returned HTML, Alpine, or narrow integration code |

The strongest rule is:

> A value the server needs should live in a successful native control or an explicitly declared request parameter.

Do not maintain a hidden Alpine-only form model that can diverge from the controls.

## 2. Baseline form

```html
<form action="/account" method="post">
    <label>
        Display name
        <input name="display_name" required>
    </label>
    <button type="submit">Save</button>
</form>
```

Build and test this path first. Then enhance it.

## 3. Add Alpine for local conditional presentation

```html
<form
    id="account-form"
    x-data="{ accountType: 'personal' }"
    action="/account"
    method="post"
>
    <label>
        Account type
        <select name="account_type" x-model="accountType">
            <option value="personal">Personal</option>
            <option value="business">Business</option>
        </select>
    </label>

    <fieldset x-cloak x-show="accountType === 'business'">
        <legend>Business details</legend>
        <label>
            Company name
            <input name="company_name">
        </label>
    </fieldset>

    <button type="submit">Save</button>
</form>
```

The `<select>` is both:

- the real submitted value; and
- the reactive source for Alpine.

Do not create a second `accountType` in a global store.

### Hidden controls and submission

`x-show` hides visually with CSS; the controls remain in the form and may still submit. Decide the server contract:

- accept and ignore irrelevant fields;
- disable the fieldset when hidden;
- or use `x-if` when the subtree should not exist.

A deliberate pattern:

```html
<fieldset
    x-cloak
    x-show="accountType === 'business'"
    :disabled="accountType !== 'business'"
>
```

A disabled fieldset’s controls are not successful controls. The server must still validate the account-type rules; the client behavior is only usability.

## 4. Add htmx submission and complete-form replacement

```html
<form
    id="account-form"
    x-data="{ accountType: 'personal' }"
    action="/account"
    method="post"
    hx-post="/account"
    hx-target="#account-form"
    hx-swap="outerHTML"
    hx-indicator="#account-saving"
    hx-disabled-elt="find fieldset, find button[type='submit']"
>
    <fieldset>
        <label>
            Account type
            <select name="account_type" x-model="accountType">
                <option value="personal">Personal</option>
                <option value="business">Business</option>
            </select>
        </label>

        <div x-cloak x-show="accountType === 'business'">
            <label>
                Company name
                <input name="company_name">
            </label>
        </div>
    </fieldset>

    <button type="submit">Save</button>
    <span id="account-saving" class="htmx-indicator" role="status">
        Saving…
    </span>
</form>
```

Why the disabling selector uses a fieldset:

- htmx 2’s `find <selector>` resolves a matching descendant, not “every descendant of this type” as a casual reading may suggest;
- disabling a containing fieldset reliably disables its descendant form controls;
- the submit button is listed separately if it sits outside the fieldset.

Use explicit IDs or a disabled fieldset rather than assuming `find input, find select, find button` disables every control.

## 5. Server response contract

### Success

Return the authoritative resulting component:

```html
<section id="account-form">
    <p role="status">Account saved.</p>
    <dl>
        <dt>Type</dt>
        <dd>Business</dd>
        <dt>Company</dt>
        <dd>Analytical Engines Ltd.</dd>
    </dl>
</section>
```

### Validation failure

Return the complete form with submitted values and accessible errors:

```html
<form
    id="account-form"
    x-data="{ accountType: 'business' }"
    action="/account"
    method="post"
    hx-post="/account"
    hx-target="#account-form"
    hx-swap="outerHTML"
>
    <div id="error-summary" role="alert" tabindex="-1">
        <p>Correct the following error:</p>
        <a href="#company-name">Company name is required.</a>
    </div>

    <label>
        Account type
        <select name="account_type" x-model="accountType">
            <option value="personal">Personal</option>
            <option value="business" selected>Business</option>
        </select>
    </label>

    <label>
        Company name
        <input
            id="company-name"
            name="company_name"
            value=""
            aria-invalid="true"
            aria-describedby="company-name-error"
        >
    </label>
    <p id="company-name-error">Company name is required.</p>

    <button type="submit">Save</button>
</form>
```

The server decides:

- accepted values;
- normalization;
- authorization;
- validation;
- error text;
- field relationships;
- whether the operation commits.

Alpine may provide early hints, but it should not duplicate the server’s complete validation schema.

## 6. Status-code policy in htmx 2.x

By default, htmx 2.x does not swap `4xx` or `5xx` responses. Choose a project policy.

### Simple validation policy

Return the form with status `200` for expected validation failures. Reserve error status codes for failures handled by a different response policy.

### Configured response policy

Configure htmx to swap selected error responses, or use a response-targets strategy, and test every status.

Whichever policy is chosen, document it. Do not allow individual features to guess.

htmx 4 changes the default and swaps `4xx`/`5xx`, which is one reason these lessons do not claim htmx 4 compatibility.

## 7. Provisional calculations

```html
<form
    x-data="{ quantity: 1, unitPrice: 18 }"
    action="/cart/lines/42"
    method="post"
    hx-post="/cart/lines/42"
    hx-target="#cart-line-42"
    hx-swap="outerHTML"
    hx-sync="this:replace"
>
    <label>
        Quantity
        <input
            type="number"
            name="quantity"
            min="1"
            x-model.number="quantity"
        >
    </label>

    <output>
        Provisional subtotal:
        $<span x-text="(quantity * unitPrice).toFixed(2)"></span>
    </output>

    <button type="submit">Update</button>
</form>
```

Alpine gives immediate feedback. The server still owns:

- current price;
- accepted quantity;
- inventory;
- discounts;
- tax;
- final total.

Label the preview “provisional,” “estimate,” or equivalent when users could mistake it for a committed amount.

## 8. Inline validation versus final validation

An input may request a server hint:

```html
<form
    action="/users"
    method="post"
    hx-post="/users"
    hx-target="#user-form"
    hx-swap="outerHTML"
    hx-sync="this:replace"
>
    <label>
        Username
        <input
            name="username"
            required
            hx-post="/users/validate-username"
            hx-trigger="change"
            hx-target="#username-hint"
            hx-sync="closest form:abort"
        >
    </label>
    <div id="username-hint" aria-live="polite"></div>
    <button type="submit">Create user</button>
</form>
```

The inline endpoint offers a hint. Final submission revalidates everything. The synchronization policy prevents an obsolete validation request from winning over submission.

## 9. File upload pattern

```html
<form
    action="/documents"
    method="post"
    enctype="multipart/form-data"
    hx-post="/documents"
    hx-encoding="multipart/form-data"
    hx-target="#upload-result"
>
    <label>
        Document
        <input type="file" name="document" required>
    </label>
    <button type="submit">Upload</button>
</form>

<div id="upload-result" aria-live="polite"></div>
```

The server owns:

- file type and size rules;
- malware scanning;
- storage;
- durable acceptance;
- authorization.

A client-side filename or preview is not evidence of acceptance.

## 10. Focus and announcement after validation

Choose one project policy:

1. render `autofocus` on the first invalid field;
2. focus an error summary after the swap;
3. emit a semantic after-swap event that a stable Alpine shell handles;
4. use a narrow `htmx:afterSettle` integration listener.

Avoid two competing focus systems.

A narrow htmx 2 integration:

```html
<script>
document.body.addEventListener('htmx:afterSettle', (event) => {
    const target = event.detail.target
    if (!target?.matches?.('#account-form')) return

    target.querySelector('[data-focus-after-swap]')?.focus()
})
</script>
```

Test with keyboard and screen reader. Do not attach an undifferentiated global listener that guesses business workflow from arbitrary markup.

## 11. Form SOP

For every mixed form:

1. build the complete non-enhanced form;
2. name every server-required control;
3. add native constraints for usability;
4. add Alpine only for local conditional UI or previews;
5. add htmx request, target, and swap;
6. define request indicator and disabling;
7. define race behavior;
8. return complete server-rendered success or error HTML;
9. preserve submitted values on error;
10. define focus and announcement;
11. test no-JavaScript submission;
12. test duplicate activation, slow responses, expired sessions, and validation failures.

## 12. Day lab: account onboarding form

Build a form with:

- personal/business account type;
- conditionally active company fields;
- a character counter for a description;
- a provisional monthly price estimate;
- a server username availability hint;
- final server validation;
- complete root replacement;
- an accessible error summary;
- a saving indicator and disabled fieldset.

Write a server response specimen for:

1. success;
2. validation failure;
3. authorization failure;
4. unexpected server error.

## Mini-tasks

### Task A: successful controls

Explain whether hidden, disabled, unnamed, and unchecked controls submit.

### Task B: status policy

Choose and document a validation status-code policy for an htmx 2 project.

### Task C: eliminate client schema duplication

Replace an Alpine object containing every server validation rule with native hints plus server-rendered validation.

### Task D: provisional truth

Add labeling that prevents a locally calculated total from being mistaken for a committed price.

## Comprehension check

1. Why is a native input the best Alpine–htmx bridge?
2. Who owns trusted validation?
3. What must a validation response preserve?
4. Why might `x-show` submit hidden controls?
5. When should a fieldset be disabled?
6. Why is `find input` not a reliable shorthand for disabling all inputs?
7. What is the difference between an inline availability hint and final validation?
8. What htmx 2 status behavior must be considered?
9. Why should success return authoritative HTML rather than `{ "ok": true }`?
10. Who owns focus after a form swap?

## Cheat sheet

```text
VALUE SERVER NEEDS        -> named control
LOCAL PREVIEW             -> Alpine bound to that control
SUBMISSION                -> htmx
FINAL VALIDATION          -> server
ERROR MARKUP              -> server template
REQUEST BUSY              -> htmx + CSS
FORM ROOT REPLACED        -> Alpine state resets
STABLE OVERLAY SHELL      -> target inner form only
```

## Exit exercise

Create an invoice adjustment form where Alpine previews an adjustment, htmx submits it, and the server returns either a canonical invoice line or a complete error form. Include an explicit conflict response for a price that changed since the page loaded.

## Exit criteria

You pass when the server can reconstruct and validate the operation from the request without reading Alpine state, and every success or failure produces usable HTML.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 5: Request State, Concurrency, Active Search, and Race-Safe Operations

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Turn ordinary requests into dependable daily operations by using htmx’s factual request state, indicators, disabling, debounce, synchronization, queue policy, and explicit retry behavior.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- display loading and busy state without a duplicate Alpine `loading` flag;
- disable the right controls for the right request;
- use trigger delay and `hx-sync` for different concurrency problems;
- design active search that cancels obsolete requests;
- distinguish request state from broader local workflow state;
- define failure, retry, and idempotency behavior.

## 1. The factual request state belongs to htmx

htmx knows when its request begins and ends. Use that truth before inventing another state machine.

Primary tools:

- `hx-indicator`;
- the `htmx-request` CSS class;
- `hx-disabled-elt`;
- `hx-sync`;
- htmx lifecycle events;
- server-rendered progress or error HTML.

A duplicate Alpine boolean can drift when a request aborts, is replaced, fails before send, or is triggered by another element.

## 2. Indicator SOP

```html
<form
    hx-post="/profile"
    hx-target="#profile"
    hx-indicator="#profile-spinner"
>
    <label>
        Display name
        <input name="display_name">
    </label>

    <button type="submit">Save</button>

    <span
        id="profile-spinner"
        class="htmx-indicator"
        role="status"
        aria-live="polite"
    >
        Saving…
    </span>
</form>
```

Example CSS:

```css
.htmx-indicator {
    display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    display: inline;
}
```

Rules:

1. Put status text near the operation it describes.
2. Use a live region only when the announcement is useful.
3. Do not hide all page content behind a full-screen spinner for a small request.
4. Keep the original control label stable when possible; changing it can confuse screen-reader users.
5. For long-running durable work, render server-owned progress rather than pretending one HTTP request is the whole workflow.

## 3. Disabling controls correctly

Use `hx-disabled-elt` to prevent accidental repeated activation.

```html
<form
    hx-post="/checkout"
    hx-target="#checkout"
    hx-indicator="#checkout-progress"
    hx-disabled-elt="find fieldset, find button[type='submit']"
>
    <fieldset>
        <!-- all checkout controls -->
    </fieldset>

    <button type="submit">Pay</button>
    <p id="checkout-progress" class="htmx-indicator" role="status">
        Processing…
    </p>
</form>
```

Why this is dependable:

- a disabled fieldset disables its descendant controls;
- the submit button is explicitly disabled;
- the selector does not pretend `find input` means every input.

Do not disable navigation or cancellation controls that must remain usable unless the workflow explicitly requires it.

## 4. Three different timing problems

### Problem A: too many events before a request

Use trigger `delay` or `throttle`.

```html
<input
    name="q"
    hx-get="/search"
    hx-trigger="input changed delay:300ms"
    hx-target="#results"
>
```

`delay` waits for a quiet period. It is debounce.

### Problem B: a newer request makes an older request obsolete

Use `hx-sync="this:replace"` or synchronize on a shared owner.

```html
<input
    name="q"
    hx-get="/search"
    hx-trigger="input changed delay:300ms"
    hx-target="#results"
    hx-sync="this:replace"
>
```

A new request aborts the current request and replaces it.

### Problem C: related elements issue competing requests

Synchronize them on a common element.

```html
<form
    hx-post="/account"
    hx-target="#account-form"
    hx-sync="this:replace"
>
    <input
        name="username"
        hx-post="/account/validate-username"
        hx-trigger="change"
        hx-target="#username-error"
        hx-sync="closest form:abort"
    >

    <div id="username-error"></div>
    <button type="submit">Save</button>
</form>
```

The input validation yields to the form submission.

## 5. `hx-sync` strategies

| Strategy | Behavior | Typical use |
| --- | --- | --- |
| `drop` | ignore a new request while one is active | protect an operation that must finish |
| `abort` | ignore this request if another is active; allow another request to abort it | low-priority validation |
| `replace` | abort current request and issue newest | active search, rapid filters |
| `queue first` | keep first waiting request | first follow-up matters |
| `queue last` | keep latest waiting request | latest adjustment matters |
| `queue all` | execute every queued request | rare; every action is meaningful |

Choose by business meaning. Do not select `queue all` merely to avoid thinking about races.

## 6. Active search pattern

A robust search has four kinds of state:

- URL: applied query and filters;
- controls: current values;
- htmx: debounce, request, replacement, history;
- Alpine: local filter-panel visibility or density.

```html
<section x-data="{ filtersOpen: false, density: 'comfortable' }">
    <button
        type="button"
        @click="filtersOpen = !filtersOpen"
        :aria-expanded="filtersOpen"
        aria-controls="search-filters"
    >
        Filters
    </button>

    <form
        id="search-form"
        x-cloak
        x-show="filtersOpen"
        action="/search"
        method="get"
        hx-get="/search"
        hx-trigger="
            input changed delay:300ms from:#search-q,
            change from:#search-status,
            submit
        "
        hx-target="#search-results"
        hx-push-url="true"
        hx-sync="this:replace"
        hx-indicator="#search-progress"
    >
        <label>
            Search
            <input id="search-q" type="search" name="q">
        </label>

        <label>
            Status
            <select id="search-status" name="status">
                <option value="">Any</option>
                <option value="open">Open</option>
                <option value="closed">Closed</option>
            </select>
        </label>

        <button type="submit">Search</button>
        <span id="search-progress" class="htmx-indicator" role="status">
            Updating results…
        </span>
    </form>

    <div
        :class="density === 'compact'
            ? 'results results--compact'
            : 'results results--comfortable'"
    >
        <div id="search-results">
            <!-- canonical server-rendered result set -->
        </div>
    </div>
</section>
```

Why exact IDs are used in the trigger:

- `from:` selectors can be broader than the visual component;
- explicit controls make the contract visible;
- newly introduced controls require a deliberate update to the trigger contract.

An alternative is to put htmx attributes directly on each triggering control with `hx-include="#search-form"`.

## 7. Stale-result protection

For ordinary active search, `hx-sync="this:replace"` is the default starting point. Still test:

1. type rapidly;
2. make the server respond out of order;
3. navigate back while a request is active;
4. submit with Enter;
5. clear the search with the browser’s search-field clear control;
6. change a select while text debounce is pending.

The displayed results and URL must represent the same applied request.

## 8. When Alpine may own request-adjacent state

Alpine may own a broader workflow that is not equivalent to “request in flight.”

Examples:

- whether a progress drawer is open;
- a reversible provisional highlight;
- a local confirmation step;
- whether the user has paused presentation updates;
- whether a retry explanation is expanded.

```html
<section x-data="{ detailsOpen: false }">
    <button type="button" @click="detailsOpen = !detailsOpen">
        Explain this operation
    </button>

    <div x-cloak x-show="detailsOpen">
        This explanation is local and remains usable while a request runs.
    </div>

    <button
        hx-post="/reports/rebuild"
        hx-indicator="#rebuild-progress"
        hx-disabled-elt="this"
    >
        Rebuild report
    </button>

    <span id="rebuild-progress" class="htmx-indicator">
        Starting rebuild…
    </span>
</section>
```

Do not call the Alpine value `loading`. Name it by its real meaning.

## 9. Idempotency and repeated activation

UI disabling is not a transaction guarantee. For sensitive actions, the server should use appropriate safeguards:

- idempotency keys;
- transaction constraints;
- unique operation identifiers;
- optimistic locking;
- duplicate detection;
- safe retry semantics.

A payment remains server-sensitive even if the button becomes disabled.

For an idempotent refresh, retry can be straightforward. For a non-idempotent mutation, retry policy must be explicit.

## 10. Network failure policy

Define a shared policy for:

- offline state;
- timeout;
- retryable GET requests;
- non-retryable or ambiguous mutations;
- preserved form drafts;
- session expiry;
- unexpected status codes;
- user-visible status announcements.

A narrow htmx 2 listener may add a generic network banner:

```html
<script>
document.body.addEventListener('htmx:sendError', () => {
    document.dispatchEvent(new CustomEvent('network-unavailable'))
})
</script>
```

Keep global handlers cross-cutting. They must not contain feature-specific success logic.

Event names differ in htmx 4; this is version-sensitive integration code.

## 11. Long-running jobs

A request that starts a job is not the same as the job being complete.

```html
<form
    hx-post="/exports"
    hx-target="#export-job"
    hx-swap="outerHTML"
>
    <button type="submit">Start export</button>
</form>

<section id="export-job">
    No export running.
</section>
```

The server may return:

```html
<section
    id="export-job"
    hx-get="/exports/8/status"
    hx-trigger="every 2s"
    hx-target="this"
    hx-swap="outerHTML"
>
    <p>Export queued.</p>
</section>
```

When complete, the server returns the same root without the polling attributes and includes the download link.

Alpine may own whether details are expanded. It must not own the factual job status.

## 12. Request-state review SOP

For every interaction, write:

```text
Can events arrive faster than requests complete?
Which request should win?
Can the operation be repeated safely?
What becomes disabled?
What remains usable?
Where is progress announced?
What happens on timeout?
What happens if the response is obsolete?
What happens after history navigation?
Is there a durable job after the initiating request?
```

## 13. Day lab: race-safe product search

Build an active product search with:

- search text;
- category and availability filters;
- 300 ms debounce;
- request replacement;
- URL updates;
- local filter-panel visibility;
- local density preference;
- a result count rendered by the server;
- a network error banner;
- direct URL support.

Use browser devtools to throttle the network and produce out-of-order responses.

## Mini-tasks

### Task A: choose the sync strategy

Select a strategy for:

- active search;
- repeated “refresh report” clicks;
- username validation versus final submit;
- a sequence of audit events where every event must be recorded;
- a quantity stepper where only the final quantity matters.

### Task B: remove duplicate loading state

Refactor an Alpine component with `loading = true/false` around an htmx request to use htmx state.

### Task C: disable safely

Repair `hx-disabled-elt="find input, find select, find button"` on a form with many controls.

### Task D: distinguish request and job state

Model an export request whose HTTP response only confirms that a server job started.

## Comprehension check

1. Why is htmx the authority for request-in-flight state?
2. What problem does trigger delay solve?
3. What different problem does `hx-sync` solve?
4. When is `replace` appropriate?
5. Why is disabling a button not enough for payments?
6. What state belongs in the URL for active search?
7. What may Alpine own beside active search?
8. How should a completed polling component stop polling?
9. What makes a global lifecycle listener appropriate?
10. Why must retry behavior differ for GET and ambiguous mutations?

## Cheat sheet

```text
TOO MANY PRE-REQUEST EVENTS      -> delay / throttle
NEWEST REQUEST SHOULD WIN        -> hx-sync="...:replace"
LOW-PRIORITY VALIDATION          -> ...:abort
REQUEST BUSY                     -> hx-indicator / htmx-request
DISABLE MANY FORM CONTROLS       -> disabled fieldset + explicit button
HTTP REQUEST STARTED A JOB       -> poll/stream server-owned job HTML
DUPLICATE SERVER OPERATION       -> server idempotency, not UI hope
```

## Exit exercise

Create a “recalculate forecast” workflow. The initial mutation starts a job, the status component polls, a stable Alpine panel may remain open, and repeated activation is prevented both in the UI and on the server. Document how timeout, retry, and duplicate submission behave.

## Exit criteria

You pass when slow, repeated, aborted, and out-of-order requests produce the same authoritative result as the happy path.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 6: Component Lifecycle, Dynamic Activation, Cleanup, Preservation, and Morphing

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Make DOM replacement deliberate. Learn when local state resets, survives, is exceptionally preserved, or is reconciled, and how to initialize and clean up components under repeated swaps.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- choose Reset, Survive, Preserve, or Reconcile before a swap;
- keep stable Alpine shells outside htmx targets;
- intentionally replace disposable Alpine roots;
- process Alpine-created `hx-*` markup with `htmx.process()`;
- use Alpine `init()` and `destroy()` for external resources;
- use htmx cleanup events for non-Alpine widgets;
- reject preservation and morphing when a better boundary solves the problem.

## 1. The governing fact

htmx replaces DOM. Alpine state is attached to component roots in that DOM.

Therefore every target needs a lifecycle decision before implementation.

## 2. The four lifecycle choices

### Reset

Replace the Alpine root and create a new instance.

Use for:

- submitted form replaced by success;
- inline editor replaced by saved record;
- row or card whose temporary menu should close;
- local draft that should clear after completion.

```html
<article id="invoice-42" x-data="{ menuOpen: false }">
    ...
    <button
        hx-get="/invoices/42/edit"
        hx-target="#invoice-42"
        hx-swap="outerHTML"
    >
        Edit
    </button>
</article>
```

The returned editor replaces the article. Reset is correct.

### Survive

Keep the Alpine root stable and replace an inner htmx target.

Use for:

- modal shell;
- drawer;
- filter panel;
- command palette;
- live panel with local expansion state.

```html
<aside x-data="{ open: false }">
    <button type="button" @click="open = !open">Cart</button>
    <section x-cloak x-show="open">
        <div id="cart-contents"></div>
    </section>
</aside>
```

htmx targets `#cart-contents`, not the `<aside>`.

### Preserve

Use `hx-preserve` only when live element identity must survive and moving the target is impractical.

```html
<div id="player" hx-preserve>
    <video controls src="/media/briefing.mp4"></video>
</div>
```

Requirements and cautions:

- the preserved element needs a stable `id`;
- the response should contain a matching ID;
- some elements do not preserve focus, caret, iframe, or media behavior perfectly;
- preservation may relocate a live node;
- history and OOB interactions need testing;
- preservation is not a substitute for ownership design.

### Reconcile

Use a morphing extension only after normal replacement and better target boundaries fail a demonstrated requirement.

Morphing adds identity, attribute reconciliation, plugin, and debugging complexity. It can preserve nodes, but it can also preserve stale local assumptions. Define IDs and state ownership first.

## 3. Lifecycle decision table

| Requirement | First choice |
| --- | --- |
| Local state should clear | Reset |
| Local shell should remain open | Survive |
| One expensive/live element must retain identity | Preserve |
| Large server component must reconcile without replacement | Reconcile, after evidence |
| Third-party widget can be isolated | Stable JavaScript island |
| No local state exists | htmx-only replacement |

## 4. Alpine initialization

Registered components may define `init()`:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('clock', () => ({
        now: new Date(),
        timer: null,

        init() {
            this.timer = window.setInterval(() => {
                this.now = new Date()
            }, 1000)
        },

        destroy() {
            window.clearInterval(this.timer)
        }
    }))
})
</script>
```

Initialization must be repeatable. A server response may create the component many times.

Do not assume a node from a previous instance still exists.

## 5. Cleanup with `destroy()`

Clean up resources that outlive ordinary DOM properties:

- intervals and timeouts;
- window/document listeners registered manually;
- observers;
- media-query listeners;
- subscriptions;
- third-party widget instances;
- browser API handles.

Alpine automatically calls a component’s `destroy()` before cleaning up that Alpine component. Repeated htmx swaps should not leak external resources.

A listener example:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('viewportWatcher', () => ({
        handler: null,
        width: window.innerWidth,

        init() {
            this.handler = () => {
                this.width = window.innerWidth
            }
            window.addEventListener('resize', this.handler)
        },

        destroy() {
            window.removeEventListener('resize', this.handler)
        }
    }))
})
</script>
```

Prefer Alpine event modifiers such as `.window` where they naturally manage listener lifetime.

## 6. Content inserted by htmx

When htmx inserts markup containing `x-data` or other Alpine directives, Alpine 3 normally observes DOM additions and initializes the new markup as a fresh component.

Operational rule:

- write initialization so it can run repeatedly;
- do not rely on removed nodes;
- verify initialization in browser integration tests;
- if using a special morph/compatibility path, follow that integration’s documentation.

Do not call `Alpine.start()` again after every swap.

## 7. Content inserted by Alpine

When Alpine creates a subtree containing `hx-*` attributes, htmx did not necessarily see those nodes during its initial processing. Process the new region after Alpine inserts it.

```html
<div
    x-data="{ mounted: false }"
    x-init="$watch('mounted', (value) => {
        if (!value) return

        $nextTick(() => {
            htmx.process($refs.remoteRegion)
        })
    })"
>
    <button type="button" @click="mounted = true">
        Mount remote panel
    </button>

    <template x-if="mounted">
        <section x-ref="remoteRegion">
            <button
                hx-get="/panel"
                hx-target="next div"
            >
                Load panel
            </button>
            <div></div>
        </section>
    </template>
</div>
```

Process only the new region. Do not repeatedly process the whole document.

A cleaner extracted method:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('remoteMount', () => ({
        mounted: false,

        mount() {
            this.mounted = true
            this.$nextTick(() => htmx.process(this.$refs.remoteRegion))
        }
    }))
})
</script>
```

## 8. Third-party widgets

A rich editor, chart, canvas, map, or drag engine may be an isolated JavaScript island.

Rules:

1. keep the island root outside ordinary htmx targets;
2. do not duplicate its internal state in Alpine;
3. expose durable values through controls or an explicit API;
4. emit semantic events;
5. document teardown if replacement is unavoidable.

Use `htmx:beforeCleanupElement` for a non-Alpine widget that must tear down before removal:

```html
<script>
document.body.addEventListener('htmx:beforeCleanupElement', (event) => {
    const element = event.detail.elt
    const editor = element.__editorInstance
    editor?.destroy()
})
</script>
```

Scope handlers narrowly. Do not scan every removed node with expensive logic unless necessary.

## 9. `x-ignore` as an isolation boundary

```html
<div x-data="{ open: true }">
    <section x-ignore id="third-party-editor">
        <!-- widget manages this subtree -->
    </section>
</div>
```

`x-ignore` tells Alpine not to initialize the subtree. It does not protect the region from htmx replacement. The htmx boundary still needs an explicit policy.

## 10. Identity rules

Distinguish:

- server IDs used for routes and canonical resources;
- DOM IDs used for targets and OOB swaps;
- stable IDs required by `hx-preserve`;
- Alpine-local generated IDs for labels, descriptions, or tabs.

Do not use a freshly generated Alpine ID for a server response that must target the same element later.

## 11. Focus and lifecycle

Replacement can remove the focused element.

For each swap, choose:

- focus remains outside target;
- returned markup uses a deliberate focus marker;
- focus moves to a stable shell;
- focus returns to the triggering control after completion;
- no focus movement is needed.

Test:

- validation loop;
- inline edit save/cancel;
- target deletion;
- history restoration;
- modal content replacement;
- polling update while the user is reading.

Do not let polling repeatedly steal focus.

## 12. Morphing decision gate

Before adding a morph extension, answer yes to all:

1. Is the replacement problem reproducible and user-visible?
2. Is moving the target boundary impossible or worse?
3. Are stable IDs defined?
4. Is the state owner documented?
5. Are Alpine and third-party widget semantics tested under morph?
6. Is focus behavior tested?
7. Is failure behavior understandable?
8. Is the extension version pinned?
9. Does the team accept the migration and debugging cost?

If not, redesign the target first.

## 13. Lifecycle SOP

For every mixed component:

```text
Alpine root:
htmx target:
Swap:
Reset / Survive / Preserve / Reconcile:
Stable IDs:
Initialization:
External resources:
Cleanup:
Dynamic Alpine-created htmx markup:
Third-party island:
Focus after swap:
History restoration behavior:
```

## 14. Day lab: live customer drawer

Build a drawer with:

- stable Alpine open/closed state;
- htmx-loaded customer details;
- an inner edit form that may swap repeatedly;
- a disposable menu inside each server-rendered customer card;
- a small third-party date picker or simulated widget with teardown;
- an Alpine-created optional remote panel that requires `htmx.process()`.

Instrument the page with console messages for `init()` and `destroy()`. Reopen and replace components repeatedly. Confirm no duplicate timers or listeners remain.

## Mini-tasks

### Task A: classify lifecycle

Choose Reset, Survive, Preserve, or Reconcile for:

- playing audio during a nearby refresh;
- an open modal with server validation;
- a row action menu after the row is saved;
- a chart with a specialized renderer;
- a long form where only one summary region changes.

### Task B: fix repeated initialization

A component calls `Alpine.start()` after every htmx swap. Replace that with the correct lifecycle design.

### Task C: dynamic activation

Mount an `x-if` subtree containing an htmx button and process only the new subtree.

### Task D: leak test

Create and destroy a timer component ten times and prove only one timer exists per live component.

## Comprehension check

1. What does Reset mean?
2. What is the preferred way to make Alpine state survive?
3. What stable requirement does `hx-preserve` have?
4. Why is morphing not a default?
5. When is `htmx.process()` needed?
6. Why should it target only the new subtree?
7. What does Alpine `destroy()` clean up?
8. When is `htmx:beforeCleanupElement` useful?
9. Does `x-ignore` prevent htmx replacement?
10. Why must polling avoid moving focus?

## Cheat sheet

```text
RESET       replace Alpine root
SURVIVE     keep Alpine root, replace inner target
PRESERVE    stable-ID live element, exceptional
RECONCILE   morph after demonstrated need
HTMX -> ALPINE MARKUP   normally fresh Alpine init
ALPINE -> HTMX MARKUP   htmx.process(newRegion)
EXTERNAL RESOURCE       init + matching destroy
RICH WIDGET             isolated stable island
```

## Exit exercise

Design a remote document editor shell. The shell remains open; server validation replaces the inner form; a third-party rich editor is isolated; a preview can refresh; and successful save resets disposable local draft flags. Document every lifecycle edge before coding.

## Exit criteria

You pass when repeated swaps produce no leaked listeners, accidental state loss, duplicate initialization, or unexplained preservation.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 7: Semantic Events and Cross-Boundary Contracts

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Connect Alpine-owned local interactions, htmx-owned requests, and server outcomes without direct state reaching, hidden watchers, or event soup.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- dispatch semantic browser events from Alpine;
- trigger htmx requests from custom events;
- emit semantic events from server responses;
- select immediate, after-swap, or after-settle timing;
- distinguish commands from facts;
- document event producers, listeners, payloads, failure behavior, and version-sensitive mechanics.

## 1. Why events are the preferred bridge

A mixed component often needs cooperation:

```text
Alpine local action
    -> semantic browser event
        -> htmx request
            -> server response
                -> htmx swap or response event
                    -> Alpine local reaction
```

Events preserve independent ownership. Alpine does not call private htmx internals, and htmx does not mutate Alpine state directly.

## 2. Event naming

Good names express domain meaning:

- `delete-confirmed`;
- `cart-updated`;
- `profile-saved`;
- `filters-committed`;
- `dialog-close-requested`;
- `job-completed`.

Poor names expose implementation:

- `run-htmx-button`;
- `set-alpine-open`;
- `swap-right-panel`;
- `update-store-value`.

Use past tense for facts (`record-saved`) and imperative/intent language for commands (`record-delete-requested`, `delete-confirmed`).

## 3. Alpine command to htmx request

```html
<div x-data="{ confirmation: '' }">
    <label>
        Type DELETE
        <input x-model="confirmation">
    </label>

    <button
        type="button"
        :disabled="confirmation !== 'DELETE'"
        @click="$dispatch('delete-confirmed', { id: 42 })"
    >
        Confirm deletion
    </button>
</div>

<button
    hidden
    type="button"
    hx-delete="/records/42"
    hx-trigger="delete-confirmed from:window"
    hx-target="#record-42"
    hx-swap="delete"
></button>
```

Ownership:

- Alpine: custom confirmation interaction;
- htmx: request trigger and deletion request;
- server: authorization and actual deletion;
- target: record component;
- event: a command that the local confirmation completed.

The `id` in the event detail is documentation in this fixed-route example; htmx is not automatically substituting that value into the URL. For dynamic IDs, use a route-specific listener element, real control, explicit request value, or a small integration handler. Do not imply payload magic that does not exist.

## 4. Server fact to Alpine reaction

The server can return a response header:

```http
HX-Trigger-After-Swap: {"profile-saved":{"id":42}}
```

Stable Alpine shell:

```html
<section
    x-data="{ open: true }"
    @profile-saved.window="open = false"
>
    <!-- modal or drawer shell -->
</section>
```

The event says what happened. Alpine decides how the local interface reacts.

Response trigger headers:

- `HX-Trigger`: when the response is received;
- `HX-Trigger-After-Swap`: after new markup is installed;
- `HX-Trigger-After-Settle`: after settling work.

Response headers are not processed on ordinary HTTP redirects. If a workflow depends on an event, return an htmx-processed response or use an explicit redirect policy.

## 5. Timing decision

### Immediate

Use when the event is independent of installed DOM:

- authentication warning;
- generic operation accepted;
- open a progress shell.

### After swap

Use when new content must exist:

- focus a returned field;
- read a returned target;
- close a modal only after canonical success markup is installed;
- announce a new server-rendered status.

### After settle

Use when final focus, layout, scrolling, or transition timing matters.

Do not choose after-settle for every event. Later timing increases coupling to presentation.

## 6. Command versus fact

Document every event as one:

### Command

A component requests that another owner attempt work.

Examples:

- `report-refresh-requested`;
- `delete-confirmed`;
- `filters-committed`.

A command may fail. Its name must not imply success.

### Fact

An authoritative outcome happened.

Examples:

- `record-deleted`;
- `profile-saved`;
- `cart-updated`.

Facts should be emitted only after the server outcome is known.

A useful workflow:

```text
delete-confirmed        command
DELETE /records/42      server attempt
record-deleted          fact after success
```

## 7. Event contract template

For each public event, record:

```text
Name:
Kind: command | fact
Producer:
Listener(s):
Payload:
Timing:
Source DOM boundary:
Does it bubble?
Failure behavior:
May it trigger a request?
Version-sensitive implementation:
Test cases:
```

Example:

```text
Name: profile-saved
Kind: fact
Producer: POST /profile success response
Listeners: profile modal shell, toast center
Payload: { id: number }
Timing: after swap
Source DOM boundary: htmx response trigger
Does it bubble? yes; listeners use .window
Failure behavior: not emitted on validation or authorization failure
May it trigger a request? no
Version-sensitive implementation: htmx 2 response-header semantics
Test cases: success, validation, slow response, repeated save
```

## 8. Avoid event soup

Warning signs:

- one event triggers another event that triggers a watcher that triggers a request;
- public events are named after element IDs;
- a generic `htmx:afterRequest` listener infers every business outcome;
- multiple components mutate the same global store after the same event;
- success is inferred by scraping response text;
- event payloads contain a complete private application state tree.

Repair:

1. reduce to one command and one authoritative fact;
2. let returned HTML carry representation changes;
3. use OOB swaps for related server-owned projections;
4. use Alpine events only for local presentation reactions;
5. document direction and failure behavior.

## 9. Event payload design

Good payloads are small and stable:

```json
{"id": 42}
```

```json
{"level": "success", "message": "Profile saved"}
```

Do not send secrets, permissions, or a full normalized entity graph.

Prefer IDs and small presentation facts. The server-rendered HTML remains the authoritative representation.

## 10. Dynamic request parameters

A semantic event payload is not automatically included in an htmx request. Use one of these explicit patterns.

### Pattern A: named hidden control

```html
<form
    id="bulk-delete-form"
    hx-delete="/records"
    hx-trigger="bulk-delete-confirmed from:window"
    hx-target="#records"
>
    <input id="selected-records" type="hidden" name="ids">
</form>
```

Alpine updates the hidden control before dispatching the command.

### Pattern B: `hx-include`

Keep related controls elsewhere and include them.

### Pattern C: narrow integration listener

Use `htmx:configRequest` in htmx 2 to add a value only when the feature contract genuinely requires it. Keep the handler scoped to a feature marker and test version migration.

### Pattern D: route-specific request element

Render one request element per record with the ID already present in the route.

The first two patterns are easiest to inspect.

## 11. Toast architecture

A small Alpine store may own transient toast presentation:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.store('toasts', {
        items: [],

        push(toast) {
            this.items.push({
                id: crypto.randomUUID(),
                ...toast
            })
        },

        remove(id) {
            this.items = this.items.filter(item => item.id !== id)
        }
    })
})
</script>

<div
    x-data
    @profile-saved.window="$store.toasts.push({
        level: 'success',
        message: 'Profile saved'
    })"
>
</div>
```

The store owns queue, timeout, and dismissal. It does not own durable notification records.

## 12. Event and OOB division

Use returned HTML or OOB swaps for authoritative representation changes:

- cart count;
- order summary;
- unread total;
- saved row;
- job status.

Use semantic events for local reactions:

- open drawer;
- close modal;
- show toast;
- move focus;
- expand a local explanation.

Do not open a drawer by sending an OOB replacement for the drawer’s local `open` state.

## 13. Testing event contracts

Test:

- event emitted exactly once;
- no fact on validation failure;
- payload shape;
- timing relative to swap;
- listener survives intended target swaps;
- listener is removed when component is destroyed;
- repeated requests do not create duplicate listeners;
- event does not accidentally trigger another request;
- htmx 2 event names are not confused with htmx 4 names.

## 14. Day lab: delete and save diplomacy

Build two workflows.

### Workflow A: custom delete confirmation

- Alpine requires the user to type the record name.
- Alpine updates a named hidden control.
- Alpine dispatches `record-delete-confirmed`.
- htmx performs the request.
- server returns authoritative list HTML and emits `record-deleted`.
- Alpine closes the dialog and a toast center announces success.

### Workflow B: profile modal

- Alpine owns modal visibility.
- htmx loads and submits the inner form.
- validation failure replaces the form and emits no success fact.
- success emits `profile-saved` after swap.
- Alpine closes and restores focus.

Write a contract for every event.

## Mini-tasks

### Task A: rename events

Replace:

- `run-button-7`;
- `set-open-false`;
- `htmx-finished`;
- `swap-cart`;
- `global-update`.

### Task B: command/fact split

Repair an event named `record-deleted` that fires before the delete request begins.

### Task C: payload plumbing

Make selected IDs enter a bulk request through a named control rather than assuming `$dispatch` payloads are request parameters.

### Task D: stop the chain

Flatten a five-hop event/watcher/request workflow into one command, one request, returned HTML, and one fact.

## Comprehension check

1. Why are semantic browser events a good Alpine–htmx bridge?
2. What is the difference between a command and a fact?
3. When should a success fact be emitted?
4. What does `HX-Trigger-After-Swap` guarantee?
5. Are response headers processed on normal redirects?
6. Does an Alpine event payload automatically become htmx parameters?
7. What should OOB swaps update?
8. What should an Alpine toast store not own?
9. Why are implementation-named events brittle?
10. What are two signs of event soup?

## Cheat sheet

```text
LOCAL INTENT                 -> Alpine $dispatch(command)
NETWORK REQUEST              -> htmx hx-trigger custom-event
AUTHORITATIVE RESULT         -> returned HTML
RELATED SERVER PROJECTION    -> OOB swap
LOCAL REACTION               -> fact event -> Alpine
PAYLOAD NEEDED BY SERVER     -> named control / hx-include / explicit handler
```

## Exit exercise

Design a “approve expense” workflow with a custom local confirmation, server authorization, an updated row, updated summary totals, modal closure, and a toast. Use one command, returned HTML plus OOB projections, and one fact. No global workflow store.

## Exit criteria

You pass when every cross-boundary action can be traced on one page from producer to listener to failure behavior.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 8: History, Navigation, Search Routes, and Progressive Enhancement

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Build interfaces whose meaningful state survives direct visits, refresh, sharing, and browser history while Alpine remains responsible only for disposable presentation state.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- decide which state belongs in the URL;
- use GET forms, `hx-push-url`, `hx-replace-url`, and `hx-boost`;
- make pushed URLs work as complete direct visits;
- keep local Alpine shell state separate from route state;
- define caching, title, focus, scroll, analytics, and history-restoration policy;
- test enhanced and non-enhanced navigation.

## 1. URL ownership

Put state in the URL when users should be able to:

- bookmark it;
- share it;
- refresh it;
- open it in a new tab;
- navigate back and forward;
- recover it after a session interruption.

Examples:

- search query;
- pagination;
- applied filters;
- selected record;
- remote tab;
- server workflow step.

Keep ephemeral presentation out of the URL:

- dropdown open/closed;
- local menu focus;
- sidebar disclosure;
- temporary unsaved selection;
- density preference unless product requirements say it is shareable.

## 2. Search GET form

```html
<form
    action="/tickets"
    method="get"
    hx-get="/tickets"
    hx-target="#ticket-results"
    hx-push-url="true"
    hx-sync="this:replace"
>
    <label>
        Search
        <input type="search" name="q" value="printer">
    </label>

    <label>
        Status
        <select name="status">
            <option value="">Any</option>
            <option value="open" selected>Open</option>
        </select>
    </label>

    <button type="submit">Search</button>
</form>

<section id="ticket-results">
    <!-- result representation for /tickets?q=printer&status=open -->
</section>
```

The URL is canonical for the applied query. The controls are its editable representation. htmx improves replacement scope.

## 3. `hx-push-url` versus `hx-replace-url`

Use `hx-push-url="true"` when the user has moved to a meaningful new location.

Examples:

- submitted search;
- page 2;
- selected remote record;
- navigable tab.

Use `hx-replace-url="true"` when updating the current location should not create another Back step.

Examples:

- canonicalizing a query;
- replacing a temporary route state;
- some continuously applied filters, if product history policy says Back should skip intermediate states.

Do not choose based only on convenience. Define expected Back-button behavior.

## 4. Direct-visit contract

Every pushed URL must work when:

1. pasted into a new tab;
2. refreshed;
3. opened without htmx;
4. restored after an htmx history cache miss;
5. requested by a crawler or accessibility tool that follows links.

A server may return:

- a complete page for a normal request;
- a fragment for `HX-Request: true`.

Both must represent the same route state.

## 5. Cache variation

If one URL has full-page and fragment variants, shared caches must not mix them.

A common response header:

```http
Vary: HX-Request
```

Also consider:

- authentication and personalization;
- `ETag` per representation;
- history restoration requests;
- CDN configuration;
- fragment-specific cache controls.

Do not use `HX-Request` as authorization. It only influences representation selection.

## 6. Remote tabs

Use Alpine-only tabs when all panels are already present and selection is temporary.

Use route-backed tabs when the selected panel:

- loads server content;
- is bookmarkable;
- changes authorization context;
- deserves browser history.

```html
<nav aria-label="Account settings">
    <a
        href="/account/profile"
        hx-get="/account/profile"
        hx-target="#settings-panel"
        hx-push-url="true"
    >
        Profile
    </a>

    <a
        href="/account/security"
        hx-get="/account/security"
        hx-target="#settings-panel"
        hx-push-url="true"
    >
        Security
    </a>
</nav>

<section id="settings-panel">
    <!-- canonical selected route representation -->
</section>
```

Alpine may manage keyboard focus or a local transition class. It must not maintain a contradictory selected route.

## 7. Pagination

```html
<a
    href="/orders?page=3"
    hx-get="/orders?page=3"
    hx-target="#orders"
    hx-push-url="true"
>
    Page 3
</a>
```

The server should render:

- result rows;
- current page;
- updated pagination links;
- document title in the complete-page representation;
- any needed OOB title or summary policy for fragments.

Do not append pages when the URL implies a single canonical page unless the product deliberately implements an infinite-feed route model.

## 8. `hx-boost`

`hx-boost` enhances ordinary links and forms while preserving their real `href` and `action`.

```html
<main hx-boost="true">
    <a href="/reports">Reports</a>
    <a href="/customers">Customers</a>
</main>
```

Before boosting a broad region, define policy for:

- target and swap;
- document title;
- focus after navigation;
- scroll position;
- analytics;
- global Alpine shells;
- page-specific initialization;
- history restoration;
- sensitive content in history snapshots;
- full-page error behavior.

Boosting is not a substitute for valid routes or complete pages.

## 9. Stable Alpine shells across navigation

A site-level shell may remain outside the boosted target:

```html
<body x-data="{ sidebarOpen: false }">
    <header>...</header>

    <aside x-cloak x-show="sidebarOpen">
        ...
    </aside>

    <main
        id="main-content"
        hx-boost="true"
        hx-target="#main-content"
        hx-select="#main-content"
        hx-swap="outerHTML"
    >
        <!-- current page main -->
    </main>
</body>
```

This is a powerful shape, but it requires testing:

- does the shell state still make sense on the new route?
- does the returned main have the same ID?
- are titles updated?
- does focus move to the new main heading?
- are route-specific scripts initialized and cleaned up?
- does history restore the correct representation?

State should survive navigation only when its meaning survives navigation.

## 10. Sensitive history

htmx history may snapshot content in browser storage. Use the project’s history policy for sensitive pages.

htmx 2 provides `hx-history="false"` to prevent a page snapshot from being stored, causing restoration to request the server instead.

Examples that deserve review:

- health or financial data;
- private messages;
- temporary authentication content;
- pages containing secrets or one-time codes.

Browser storage is not a place for Alpine-owned secrets either.

## 11. Focus and scroll policy

After a partial navigation:

- move focus to the new main heading or a stable landmark when appropriate;
- preserve focus for small in-place updates;
- avoid unexpected scroll jumps;
- allow the browser’s normal link and history behavior where possible;
- announce meaningful content changes.

A narrow server event can indicate `page-content-installed`, or a global integration listener can apply one documented navigation policy. Avoid feature-specific logic in a generic handler.

## 12. History testing matrix

Test each route with:

| Scenario | Expected |
| --- | --- |
| direct visit | complete usable page |
| htmx click | correct partial replacement |
| refresh | same route state |
| Back | previous meaningful route |
| Forward | restored next route |
| new tab | complete page |
| history cache miss | server restores route |
| JavaScript disabled | link/form works |
| expired session | safe authorization path |
| slow response | no stale route/content mismatch |

## 13. Progressive enhancement SOP

1. define route and complete page;
2. implement real links/forms;
3. render server success and error states;
4. add htmx target and swap;
5. add URL push/replace behavior;
6. configure cache variants;
7. add Alpine only for local ergonomics;
8. define title, focus, scroll, and analytics;
9. test history and direct visits;
10. review sensitive snapshot policy.

## 14. Day lab: ticket explorer

Build:

- `/tickets?q=&status=&page=`;
- a complete direct page;
- a fragment result path using the same route;
- search form with URL updates;
- pagination links;
- route-backed ticket details;
- a local Alpine filter disclosure;
- a local density preference;
- Back/Forward behavior;
- a focus policy after route changes.

Record whether each interaction pushes or replaces history and why.

## Mini-tasks

### Task A: route or local?

Classify:

- open sidebar;
- selected remote account tab;
- result density;
- page number;
- unsaved note;
- selected report date range;
- open menu;
- current customer ID.

### Task B: repair fake routing

Replace an Alpine store that owns `currentPage` and fetches JSON with real URLs and htmx history.

### Task C: direct-visit test

Take every pushed URL in a sample page and write the complete-page response it requires.

### Task D: cache policy

Document headers for a route that returns full and fragment representations.

## Comprehension check

1. What kind of state belongs in the URL?
2. When should `hx-replace-url` be used?
3. What must every pushed URL support?
4. Why might `Vary: HX-Request` be needed?
5. What may Alpine own in a route-backed tab component?
6. What policy must precede broad `hx-boost`?
7. Why can stable shell state become wrong after navigation?
8. What does `hx-history="false"` address?
9. Why is direct navigation a server responsibility?
10. What must Back and Forward tests verify?

## Cheat sheet

```text
BOOKMARKABLE / SHAREABLE      -> URL
NEW MEANINGFUL LOCATION       -> hx-push-url
UPDATE CURRENT LOCATION       -> hx-replace-url
COMPLETE DIRECT ROUTE         -> server page
PARTIAL ENHANCEMENT           -> htmx target
LOCAL DISCLOSURE / DENSITY    -> Alpine
FULL + FRAGMENT SAME URL      -> cache variation policy
SENSITIVE PAGE                -> history snapshot review
```

## Exit exercise

Design a route-backed analytics explorer with date range, segment, page, and selected report in the URL. Keep chart display options and panel disclosure local. Specify complete-page, fragment, cache, title, focus, scroll, and history behavior.

## Exit criteria

You pass when any meaningful application location can be copied, refreshed, and restored without relying on an Alpine store.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 9: Accessible Modals, Drawers, Remote Forms, and Inline Editing

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Combine stable local overlay behavior with server-loaded content, repeated validation swaps, deliberate focus, and disposable inline editors.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- keep modal or drawer state stable while htmx replaces inner server content;
- use a native `<dialog>` or a tested overlay implementation;
- load remote forms, preserve validation loops, and close only on authoritative success;
- restore focus;
- design inline editors whose local state resets correctly;
- distinguish local overlay mechanics from server workflow state.

## 1. Overlay ownership

A remote modal has multiple owners:

| Concern | Owner |
| --- | --- |
| open/closed state | Alpine or native dialog state coordinated by Alpine |
| focus entry and restoration | Alpine/native dialog/integration policy |
| form HTML | server template |
| form submission and replacement | htmx |
| validation and authorization | server |
| close-on-success fact | server response event |
| backdrop and transition | CSS/Alpine, one animation owner |
| durable record | server/database |

The modal shell normally remains outside the htmx target.

## 2. Why use `<dialog>` as the baseline

A native `<dialog>` provides useful platform behavior for modal presentation, including a modal top layer and browser-managed focus behavior. It still requires:

- a label or heading;
- a close control;
- focus restoration policy;
- Escape/cancel handling;
- testing across supported browsers and assistive technologies;
- a polyfill or alternate implementation if project support requires it.

Do not set `document.body.inert = true` from a simplistic Alpine effect when the dialog itself is inside the body. That can disable the interface you are trying to use.

## 3. Stable remote dialog shell

```html
<section
    x-data="remoteDialog"
    @record-saved.window="closeAfterSuccess"
    @modal-content-loaded.window="focusLoadedContent"
>
    <button
        type="button"
        x-ref="opener"
        hx-get="/records/42/edit"
        hx-target="#record-dialog-body"
        hx-swap="innerHTML"
        hx-indicator="#record-dialog-loading"
        @click="open"
    >
        Edit record
    </button>

    <dialog
        x-ref="dialog"
        @cancel="handleCancel"
        @close="afterClose"
        aria-labelledby="record-dialog-title"
    >
        <header>
            <h2 id="record-dialog-title">Edit record</h2>
            <button type="button" @click="close">Close</button>
        </header>

        <p
            id="record-dialog-loading"
            class="htmx-indicator"
            role="status"
        >
            Loading editor…
        </p>

        <div id="record-dialog-body">
            <!-- htmx-owned server content -->
        </div>
    </dialog>
</section>
```

Registered Alpine component:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('remoteDialog', () => ({
        lastOpener: null,

        open() {
            this.lastOpener = this.$refs.opener
            if (!this.$refs.dialog.open) {
                this.$refs.dialog.showModal()
            }
        },

        close() {
            if (this.$refs.dialog.open) {
                this.$refs.dialog.close()
            }
        },

        handleCancel(event) {
            event.preventDefault()
            this.close()
        },

        afterClose() {
            this.$nextTick(() => this.lastOpener?.focus())
        },

        closeAfterSuccess() {
            this.close()
        },

        focusLoadedContent() {
            this.$nextTick(() => {
                this.$refs.dialog
                    .querySelector('[data-dialog-initial-focus]')
                    ?.focus()
            })
        }
    }))
})
</script>
```

Notes:

- the shell and Alpine root survive;
- only `#record-dialog-body` is replaced;
- opening immediately can show a loading state;
- validation failure leaves the dialog open;
- the server emits `record-saved` only on authoritative success;
- the server may emit `modal-content-loaded` after swap when initial focus should move.

For multiple opener buttons, store the actual `$el` in the click handler or dispatch an open command with the source. Do not hardcode one opener reference in a reusable component.

## 4. Remote form contract

The load response:

```html
<form
    id="record-form"
    action="/records/42"
    method="post"
    hx-post="/records/42"
    hx-target="#record-form"
    hx-swap="outerHTML"
>
    <label>
        Title
        <input
            name="title"
            value="Quarterly review"
            data-dialog-initial-focus
        >
    </label>

    <button type="submit">Save</button>
</form>
```

Validation response returns the same root and no success fact:

```html
<form
    id="record-form"
    action="/records/42"
    method="post"
    hx-post="/records/42"
    hx-target="#record-form"
    hx-swap="outerHTML"
>
    <div role="alert" tabindex="-1" data-dialog-initial-focus>
        Correct the highlighted field.
    </div>

    <label>
        Title
        <input
            name="title"
            value=""
            aria-invalid="true"
            aria-describedby="title-error"
        >
    </label>
    <p id="title-error">Title is required.</p>

    <button type="submit">Save</button>
</form>
```

Success may return a confirmation fragment or updated record projection and headers:

```http
HX-Trigger-After-Swap: {"record-saved":{"id":42}}
```

If the row outside the dialog also changes, return it as an OOB swap or target the authoritative row through a separate response design.

## 5. Do not close on request completion

An htmx request can complete with:

- validation failure;
- authorization failure;
- conflict;
- unexpected error;
- success.

Therefore a generic `htmx:afterRequest` listener must not close the dialog. Close on an explicit success fact.

## 6. Drawer pattern

A drawer is often a stable Alpine shell with an htmx interior.

```html
<aside
    x-data="{ open: false }"
    @cart-updated.window="open = true"
>
    <button
        type="button"
        @click="open = !open"
        :aria-expanded="open"
        aria-controls="cart-drawer"
    >
        Cart
    </button>

    <section
        id="cart-drawer"
        x-cloak
        x-show="open"
        x-transition
        @keydown.escape.window="open = false"
    >
        <button type="button" @click="open = false">
            Close cart
        </button>

        <div id="cart-contents">
            <!-- server-owned -->
        </div>
    </section>
</aside>
```

Production requirements may include:

- focus trap if it behaves as a modal;
- focus restoration;
- scroll locking;
- accessible name;
- background interaction policy;
- responsive behavior;
- one animation owner.

Use the Alpine Focus plugin or a tested design-system component when requirements exceed a simple non-modal drawer.

## 7. Inline edit lifecycle

A record row may be replaced completely.

View state:

```html
<article id="customer-42" x-data="{ menuOpen: false }">
    <h2>Ada Lovelace</h2>

    <button type="button" @click="menuOpen = !menuOpen">
        Actions
    </button>

    <div x-cloak x-show="menuOpen">
        <button
            hx-get="/customers/42/edit"
            hx-target="#customer-42"
            hx-swap="outerHTML"
        >
            Edit
        </button>
    </div>
</article>
```

Edit response:

```html
<article id="customer-42">
    <form
        action="/customers/42"
        method="post"
        hx-post="/customers/42"
        hx-target="#customer-42"
        hx-swap="outerHTML"
    >
        <label>
            Name
            <input
                name="name"
                value="Ada Lovelace"
                data-focus-after-swap
            >
        </label>

        <button type="submit">Save</button>
        <button
            type="button"
            hx-get="/customers/42"
            hx-target="#customer-42"
            hx-swap="outerHTML"
        >
            Cancel
        </button>
    </form>
</article>
```

The old Alpine menu state disappears. Correct.

## 8. Focus after inline replacement

A narrow integration listener can focus a marker:

```html
<script>
document.body.addEventListener('htmx:afterSettle', (event) => {
    const target = event.detail.target
    if (!target?.matches?.('[id^="customer-"]')) return

    target.querySelector('[data-focus-after-swap]')?.focus()
})
</script>
```

Cautions:

- keep the selector feature-specific;
- do not focus on polling updates;
- after save, decide whether focus moves to the updated heading, Edit button, or remains near the operation;
- after delete, move focus to a logical neighbor or list heading;
- test browser history restoration.

## 9. Conflict handling

An inline editor may be stale.

Server response should explain:

- what changed;
- whether the user’s draft is preserved;
- how to reload or compare;
- whether resubmission is safe.

Example:

```html
<article id="customer-42">
    <div role="alert">
        This customer was updated by another user.
    </div>

    <form ...>
        <!-- submitted values preserved -->
        <button name="resolution" value="reload" type="submit">
            Reload current record
        </button>
        <button name="resolution" value="overwrite" type="submit">
            Overwrite, if authorized
        </button>
    </form>
</article>
```

Alpine may reveal a local comparison panel. The server owns the conflict decision.

## 10. Overlay and editor SOP

For every remote overlay:

```text
Stable shell:
Inner target:
Open trigger:
Loading behavior:
Initial focus:
Validation focus:
Success fact:
Close behavior:
Focus restoration:
Escape/cancel:
Background interaction:
Error behavior:
```

For every inline editor:

```text
View root:
Edit root:
Swap:
Disposable Alpine state:
Initial focus:
Cancel response:
Validation response:
Conflict response:
Post-delete focus:
```

## 11. Day lab: customer management

Implement:

- a remote native dialog for editing customer details;
- server-rendered validation;
- close-on-success event;
- focus restoration;
- an outside customer row updated via OOB swap;
- a non-modal cart-style drawer;
- an inline status editor that replaces its whole row;
- conflict handling.

Test with keyboard only.

## Mini-tasks

### Task A: repair premature close

A modal closes on `htmx:afterRequest`. Change it to close only on `record-saved`.

### Task B: move the target

A modal root is replaced on every validation error. Keep the shell stable.

### Task C: focus map

Write expected focus for load, validation, success, cancel, delete, and network error.

### Task D: reset intentionally

Explain why an inline row menu should disappear after the row is replaced.

## Comprehension check

1. Who owns modal visibility?
2. Why should the shell stay outside the form target?
3. Why is `<dialog>` a useful baseline?
4. Why is a generic request-complete event insufficient for closing?
5. What should validation failure return?
6. What restores focus after close?
7. When should an inline editor replace its Alpine root?
8. What should happen to focus after deletion?
9. Who owns conflict resolution?
10. Why should polling never steal focus?

## Cheat sheet

```text
MODAL OPEN/CLOSE           -> Alpine/native dialog
REMOTE BODY                -> htmx target
VALIDATION                 -> server-rendered form
CLOSE                      -> explicit server success fact
FOCUS RESTORE              -> remembered opener
INLINE EDIT                -> outerHTML complete row
MENU STATE AFTER SAVE      -> reset
CONFLICT                   -> server outcome + usable HTML
```

## Exit exercise

Build a “review expense” dialog that loads remotely, supports validation and conflict resolution, updates the expense row and summary total, closes only on success, shows a toast, and restores focus to the exact opener.

## Exit criteria

You pass when the complete workflow is usable by keyboard, every focus move is intentional, and no local shell state is destroyed by an inner validation swap.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 10: Out-of-Band Updates, Toasts, Notifications, Jobs, and Live Panels

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Coordinate several authoritative server projections and immediate local presentation without building a global client data store.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- use OOB swaps for directly related server-authoritative projections;
- use Alpine for transient toasts and local panel behavior;
- model notification records separately from notification presentation;
- poll or stream authoritative job HTML;
- avoid OOB sprawl, store sprawl, and high-frequency HTML misuse;
- define stop conditions, cleanup, focus, and failure behavior.

## 1. Two kinds of consequences

A server operation may cause:

### Authoritative representation changes

Use normal returned HTML or OOB swaps:

- cart contents;
- cart count;
- order total;
- notification list;
- unread count;
- saved row;
- summary totals;
- job state.

### Local presentation reactions

Use Alpine and semantic events:

- open a drawer;
- close a dialog;
- show a toast;
- expand details;
- focus a result;
- animate a local shell.

This division is one of the strongest combined patterns.

## 2. OOB cart response

Request:

```html
<button
    hx-post="/cart/items/42"
    hx-target="#cart-contents"
    hx-swap="outerHTML"
>
    Add to cart
</button>
```

Response:

```html
<section id="cart-contents">
    <!-- complete updated cart -->
</section>

<span id="cart-count" hx-swap-oob="outerHTML">3</span>

<section id="order-summary" hx-swap-oob="outerHTML">
    <!-- authoritative summary -->
</section>
```

Response header:

```http
HX-Trigger-After-Swap: {"cart-updated":{"itemId":42}}
```

Alpine shell:

```html
<aside
    x-data="{ open: false }"
    @cart-updated.window="open = true"
>
    <!-- stable drawer -->
</aside>
```

The response updates three server projections and separately asks the local shell to react.

## 3. OOB suitability test

Use OOB when all are true:

1. the regions are direct consequences of the same server transaction;
2. each region is an authoritative server projection;
3. each has stable identity;
4. the response remains understandable;
5. the number of regions is bounded;
6. testing the response is straightforward.

Do not use OOB to:

- open drawers;
- switch local tabs;
- toggle animations;
- mutate unrelated page regions;
- create page-wide choreography.

If one action updates ten unrelated zones, reconsider the page and endpoint design.

## 4. Toast center

A toast is transient presentation, not durable truth.

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.store('toastCenter', {
        items: [],

        push({ level = 'info', message, timeout = 5000 }) {
            const id = crypto.randomUUID()
            this.items.push({ id, level, message })

            window.setTimeout(() => this.remove(id), timeout)
        },

        remove(id) {
            this.items = this.items.filter(item => item.id !== id)
        }
    })
})
</script>

<section
    x-data
    aria-live="polite"
    aria-label="Notifications"
    @profile-saved.window="$store.toastCenter.push({
        level: 'success',
        message: 'Profile saved.'
    })"
>
    <template x-for="toast in $store.toastCenter.items" :key="toast.id">
        <article>
            <p x-text="toast.message"></p>
            <button type="button" @click="$store.toastCenter.remove(toast.id)">
                Dismiss
            </button>
        </article>
    </template>
</section>
```

Production notes:

- do not put essential error recovery only in a disappearing toast;
- pause or extend timeout on focus/hover if required;
- allow dismissal;
- avoid flooding;
- use an appropriate live-region politeness level;
- keep durable notification records on the server.

## 5. Durable notification center

Ownership:

- server: records, read/unread state, permissions;
- htmx: load, paginate, mark read, poll/stream HTML;
- OOB: unread count;
- Alpine: panel open state, local filters, toast presentation;
- URL: a navigable notification detail if product-relevant.

```html
<aside x-data="{ open: false }">
    <button
        type="button"
        @click="open = !open"
        :aria-expanded="open"
    >
        Notifications
        <span id="unread-count">4</span>
    </button>

    <section x-cloak x-show="open">
        <div
            id="notification-list"
            hx-get="/notifications"
            hx-trigger="notification-refresh-requested from:window"
            hx-target="this"
        >
            <!-- authoritative list -->
        </div>
    </section>
</aside>
```

Mark-read response:

```html
<li id="notification-42">
    <!-- updated read representation -->
</li>

<span id="unread-count" hx-swap-oob="outerHTML">3</span>
```

## 6. Polling job component

```html
<section x-data="{ detailsOpen: false }">
    <button type="button" @click="detailsOpen = !detailsOpen">
        Job details
    </button>

    <div
        id="job-42"
        hx-get="/jobs/42/status"
        hx-trigger="every 2s"
        hx-target="this"
        hx-swap="outerHTML"
    >
        <p>Processing 48%…</p>
    </div>

    <div x-cloak x-show="detailsOpen">
        Local explanatory content
    </div>
</section>
```

Completed response:

```html
<div id="job-42">
    <p role="status">Export complete.</p>
    <a href="/exports/42/download">Download</a>
</div>
```

The completed root omits polling attributes, so polling stops.

The Alpine shell survives because only `#job-42` is targeted.

## 7. “Pause” must have one clear meaning

A local Alpine flag called `paused` is ambiguous.

Possible meanings:

- stop network polling;
- keep polling but freeze chart animation;
- stop announcements;
- hide updates;
- postpone replacing a view while the user reads.

Name and implement the exact behavior.

If the requirement is to stop requests, redesign the trigger explicitly—for example, replace the polling component with a non-polling representation or trigger polling through a controlled custom event. Do not merely hide the component and assume transport stopped.

## 8. SSE and WebSockets

In stable htmx 2.x, SSE and WebSocket support are extensions. Use them when the server delivers authoritative HTML events.

Ownership still applies:

- server: event and durable state;
- htmx extension: transport and HTML installation;
- Alpine: local expansion, selection, mute, or presentation;
- plain JavaScript: high-frequency numerical visualization.

Pin extension versions and document lifecycle, reconnection, authentication, and teardown.

htmx 4 significantly changes extension mechanics; follow the migration guide.

## 9. High-frequency updates

Do not force constant HTML replacement for:

- 60 fps charts;
- canvas animation;
- audio visualization;
- rapidly changing geometry;
- large streaming numeric datasets.

Use a specialized renderer as a stable JavaScript island. htmx can update surrounding labels, controls, and authoritative snapshots at a lower frequency.

## 10. Multi-region testing

Test:

- every OOB target exists or failure is handled;
- IDs are unique;
- primary and OOB roots match expected swaps;
- a missing optional region does not corrupt the page;
- fact events fire after intended swaps;
- local shell state survives;
- focus is not stolen;
- back/forward remains coherent;
- polling stops;
- repeated notifications do not create duplicate toasts;
- server authorization applies to every projection.

## 11. Server response review

A reviewer should be able to read one response and answer:

```text
Primary target:
Primary representation:
OOB regions:
Why each OOB region is directly related:
Fact events:
Event timing:
Status:
History effect:
Focus effect:
Retry/idempotency:
```

## 12. Day lab: operations notification and export center

Build:

1. stable notification drawer;
2. server list and unread count;
3. mark-read OOB updates;
4. transient toast on new notification;
5. export-start form;
6. polling job card;
7. completed download representation;
8. local job-details disclosure;
9. explicit network-error and session-expiry behavior.

Do not introduce a client entity store.

## Mini-tasks

### Task A: OOB or event?

Choose for:

- updated cart count;
- open cart drawer;
- updated order total;
- show saved toast;
- replace saved row;
- expand help text.

### Task B: stop polling

Change a completed job response so it cannot continue polling.

### Task C: separate durable and transient notifications

Move unread truth from an Alpine store to server HTML while keeping toast presentation local.

### Task D: prevent OOB sprawl

Refactor a response that updates twelve unrelated page regions.

## Comprehension check

1. What should OOB swaps represent?
2. What should semantic events represent?
3. Why is opening a drawer not an OOB concern?
4. Who owns unread notification truth?
5. Who owns a toast timeout?
6. How does a polling component stop?
7. Why is a generic `paused` flag dangerous?
8. When should a specialized renderer replace HTML swaps?
9. What extension concern is version-sensitive?
10. What must multi-region tests prove?

## Cheat sheet

```text
PRIMARY AUTHORITATIVE REGION      -> normal swap
RELATED AUTHORITATIVE PROJECTION  -> OOB
LOCAL PRESENTATION REACTION       -> Alpine event listener
DURABLE NOTIFICATION              -> server
TOAST QUEUE                       -> Alpine
JOB TRUTH                         -> server HTML
POLLING                           -> htmx
HIGH-FREQUENCY GRAPHICS           -> JS island
```

## Exit exercise

Design a live incident-response panel. Server events update incident status, active responder count, and timeline. Alpine owns expanded details, local sound preference, and toast presentation. A chart is an isolated renderer. Document which updates use primary swap, OOB, events, polling/SSE, and plain JavaScript.

## Exit criteria

You pass when one server transaction can coherently update several projections without a client store, and local presentation remains independent.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 11: Production Security, Error Doctrine, Testing, Naming, and Daily Operations

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Turn the patterns into a maintainable production discipline with security boundaries, CSP choices, error policy, component contracts, test matrices, naming conventions, review procedures, and htmx-version controls.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- identify what the browser can never authorize;
- protect against XSS, CSRF, unsafe raw HTML, and executable untrusted markup;
- choose a CSP-compatible Alpine strategy;
- distinguish validation, network, authorization, and unexpected errors;
- test full pages, fragments, lifecycle, races, history, and accessibility;
- use a standard component contract and code-review checklist;
- track htmx 2 versus htmx 4 migration risk.

## 1. Security authority

The server remains responsible for:

- authentication;
- authorization;
- CSRF enforcement;
- validation and normalization;
- escaping and sanitization;
- rate limits;
- transaction integrity;
- conflict handling;
- audit logging;
- file scanning;
- session policy.

A hidden or disabled control is not authorization. An Alpine permission flag is not authorization. An htmx request header is not authorization.

## 2. XSS and untrusted markup

Both Alpine and htmx make HTML more expressive. If an attacker can inject arbitrary HTML attributes, they may inject behavior.

Rules:

1. escape untrusted values in server templates;
2. sanitize deliberately allowed rich HTML with an allowlist;
3. remove dangerous script and behavior attributes during sanitization;
4. avoid `x-html` for untrusted content;
5. do not mark user HTML raw merely because it will enter through htmx;
6. consider `hx-disable` around intentionally inert untrusted HTML in htmx 2 as defense in depth;
7. consider `x-ignore` when Alpine must not process a trusted isolated widget, but do not mistake it for sanitization.

```html
<div hx-disable>
    <!-- escaped or sanitized user content -->
</div>
```

`hx-disable` does not repair unsafe server rendering. It only prevents htmx processing in that subtree.

Note: htmx 4 renames/reassigns some disable-related attributes; check the migration guide.

## 3. CSRF

Use the framework’s standard CSRF defense.

Preferred:

- hidden token in real forms when the framework supports it;
- same-site cookie policy;
- server token validation;
- explicit headers for non-form methods when required.

htmx can include a header:

```html
<body hx-headers='{"X-CSRF-Token": "SERVER_RENDERED_TOKEN"}'>
```

But request headers alone are not a complete CSRF design. Consider boosted navigation and whether the token-bearing ancestor is replaced.

Never expose a secret token through Alpine state unnecessarily.

## 4. Content Security Policy

The normal Alpine build evaluates attribute expressions in a way that conflicts with strict CSP policies that disallow `'unsafe-eval'`.

Options:

1. use Alpine’s CSP build;
2. use a project CSP that deliberately allows the required behavior;
3. extract behavior into registered components and follow CSP-build syntax limitations;
4. reduce inline scripting;
5. validate plugins against the chosen build.

The Alpine CSP build supports most common expressions but not every JavaScript feature. Test the actual application rather than assuming equivalence.

htmx also supports CSP-conscious operation, but inline `hx-on` scripting and injected raw HTML require review. Prefer semantic events and registered JavaScript listeners when strict CSP is a goal.

## 5. Error taxonomy

### Expected validation or business rejection

Return usable server-rendered HTML with:

- submitted values;
- field errors;
- summary where appropriate;
- accessible relationships;
- clear next action.

### Authorization or session expiry

Use a consistent global policy only if it is genuinely cross-cutting:

- redirect to login;
- replace a session-expired panel;
- show a global reauthentication dialog;
- preserve safe drafts;
- avoid replaying unsafe mutations automatically.

### Network failure or timeout

Define:

- retry eligibility;
- offline message;
- draft preservation;
- ambiguous mutation handling;
- status announcement;
- recovery action.

### Unexpected server error

Return a safe, traceable representation. Do not expose stack traces or secrets. Include correlation IDs where the server supports them.

### Alpine/local JavaScript error

A local error must not leave durable state ambiguous. Keep methods small, clean up resources, and avoid optimistic UI that cannot be reconciled.

## 6. Central listeners: narrow and cross-cutting

Reasonable global htmx lifecycle uses:

- request tracing;
- analytics;
- session-expiry handling;
- network banner;
- focus policy for boosted navigation;
- third-party teardown;
- security logging.

Unreasonable:

- “after every request, inspect text and decide which business workflow succeeded”;
- global store mutation for every response;
- feature routing hidden in a listener.

Scope by target, marker, status, or declared event.

## 7. Testing layers

### Server tests

Verify:

- authorization;
- validation;
- transaction and conflict behavior;
- full-page response;
- htmx fragment response;
- response status policy;
- response headers;
- OOB fragments;
- cache variation;
- direct route behavior;
- escaped output.

### Alpine unit/component tests

Verify:

- local state transitions;
- keyboard behavior;
- focus entry/restoration;
- provisional calculations;
- event dispatch/reaction;
- store queue behavior;
- `init()` and `destroy()` cleanup.

### Browser integration tests

Verify:

- stable shells survive;
- disposable roots reset;
- swapped-in Alpine components initialize;
- Alpine-created htmx markup is processed;
- indicators and disabling;
- races;
- validation loops;
- OOB updates;
- fact-event timing;
- polling stop;
- Back/Forward;
- no-JavaScript path where required;
- keyboard and screen-reader-critical behavior.

## 8. Failure matrix

For each feature, test at least:

| Case | Expected |
| --- | --- |
| success | canonical HTML installed |
| validation failure | values/errors preserved |
| authorization denied | safe consistent policy |
| stale conflict | explicit resolution UI |
| network offline | recoverable message |
| timeout | retry policy |
| rapid repeated action | synchronized/idempotent |
| history restore | correct route representation |
| component removed | cleanup runs |
| JavaScript unavailable | baseline route/form works |

## 9. Naming and organization

Suggested project structure:

```text
templates/
  pages/
  components/
  fragments/

static/
  js/
    alpine-components/
    htmx-integration/
    islands/
  css/

tests/
  server/
  browser/
```

Name server targets by domain role:

- `#cart-contents`;
- `#search-results`;
- `#profile-form`;
- `#job-status`.

Name Alpine components by local behavior:

- `remoteDialog`;
- `filterPanel`;
- `toastCenter`;
- `combobox`.

Name events by domain meaning:

- `profile-saved`;
- `cart-updated`;
- `filters-committed`.

Avoid public names such as `alpineThing`, `htmxSwapNow`, or `rightDiv`.

## 10. Standard component contract

Every mixed component should document:

### Identity

- server resource ID;
- DOM target IDs;
- OOB IDs;
- preservation or generated local IDs.

### State ownership

- durable;
- URL;
- control;
- request;
- Alpine local;
- derived/provisional;
- third-party island.

### Replacement

- target;
- swap;
- Reset/Survive/Preserve/Reconcile;
- response root.

### Requests

- method and route;
- triggers;
- inputs;
- concurrency;
- indicators;
- disabling;
- idempotency.

### Events

- command/fact;
- producer/listener;
- payload;
- timing;
- failure.

### History

- push/replace/none;
- direct visit;
- cache;
- sensitive snapshots.

### Accessibility

- labels/roles;
- focus;
- keyboard;
- announcements;
- reduced motion.

### Errors and cleanup

- validation;
- network;
- authorization;
- conflict;
- destroy/teardown.

## 11. Code review SOP

Review in this order:

1. **Server truth:** Is authorization and validation complete?
2. **Native semantics:** Are links, forms, controls, and headings correct?
3. **State inventory:** Is every value owned once?
4. **DOM boundaries:** Is every writer explicit?
5. **Request contract:** Method, trigger, inputs, target, swap, response root?
6. **Concurrency:** What wins under overlap?
7. **Lifecycle:** Reset or survive? Cleanup?
8. **Events:** One-way, semantic, documented?
9. **History:** Direct URL and Back/Forward?
10. **Accessibility:** Focus, keyboard, announcement?
11. **Security:** Escaping, CSRF, CSP, untrusted HTML?
12. **Failure:** Validation, network, session, conflict?
13. **Complexity:** Could HTML/CSS or a smaller boundary replace the cleverness?
14. **Version:** Is htmx 2 syntax accidentally mixed with htmx 4?

## 12. Anti-pattern catalog

### Alpine shadow backend

Symptoms: entities, permissions, routing, business rules, and reconciliation in stores.

### htmx for local trivia

Symptoms: requests for dropdowns, already-present tabs, or local previews.

### Two loading systems

Symptoms: `loading` boolean and htmx indicator disagree.

### Two form models

Symptoms: input, Alpine object, and request parameter differ.

### Broad swaps

Symptoms: unrelated focus and local state vanish.

### Competing collection writers

Symptoms: `x-for` and htmx both manage server records.

### Event maze

Symptoms: hidden multi-hop chains.

### OOB sprawl

Symptoms: one response choreographs unrelated page regions.

### Blanket preservation or morphing

Symptoms: identity magic compensates for a poor target.

### Attribute overload

Symptoms: a single tag contains endpoints, long expressions, business rules, and DOM algorithms.

Repair with registered Alpine components, template helpers, server partials, or a JavaScript island.

## 13. htmx 2 / htmx 4 version gate

Verified on the package date:

- stable htmx documentation identifies 2.x as latest stable and shows 2.0.10;
- htmx 4 is beta;
- htmx 4 changes event names;
- inheritance becomes explicit by default;
- `4xx`/`5xx` swapping defaults change;
- disable-related attribute names change;
- extension mechanics change.

Repository controls:

1. pin htmx version;
2. record version in architecture docs;
3. keep lifecycle listeners in a dedicated module;
4. prohibit mixed major-version syntax;
5. run the htmx 4 upgrade checker before migration;
6. test error swapping and inheritance during migration;
7. update every onboarding example when the project baseline changes.

## 14. Operational troubleshooting sequence

When a mixed component fails:

1. reproduce without Alpine if the problem is server request/swap;
2. reproduce without htmx if the problem is local interaction;
3. inspect actual request method, URL, parameters, status, and response;
4. inspect target and response root;
5. confirm which nodes were removed;
6. confirm Alpine root reset/survival;
7. log htmx lifecycle events narrowly;
8. inspect duplicate listeners/timers;
9. test a direct route;
10. test slow network and repeated action;
11. check version-specific syntax;
12. reduce to the smallest ownership conflict.

## 15. Day lab: production review

Take one feature from Days 5–10 and produce:

- component contract;
- threat review;
- CSP decision;
- status-code policy;
- failure matrix;
- server test list;
- Alpine test list;
- browser test list;
- accessibility test;
- code-review report;
- htmx 4 migration notes.

Then intentionally inject three faults:

1. target the stable Alpine root;
2. return the wrong `outerHTML` root;
3. add a duplicate Alpine loading flag.

Diagnose them using the troubleshooting sequence.

## Mini-tasks

### Task A: threat boundary

Explain why a hidden Delete button does not prevent unauthorized deletion.

### Task B: raw HTML

Design safe handling for user-authored rich text.

### Task C: global listener review

Classify five lifecycle listeners as cross-cutting or feature-specific.

### Task D: version audit

Find every htmx event name and inherited attribute in a sample component and mark migration risk.

## Comprehension check

1. What can the browser never authorize?
2. Why is `x-html` dangerous with untrusted content?
3. What does `hx-disable` provide?
4. What CSP issue does standard Alpine raise?
5. What four major error categories should be separated?
6. What makes a global listener acceptable?
7. What must server tests cover beyond JSON/data?
8. What must browser tests prove about lifecycle?
9. Name three htmx 4 migration changes.
10. What is the first troubleshooting split?

## Cheat sheet

```text
AUTH / VALIDATION / TRANSACTION  -> server
ESCAPE UNTRUSTED CONTENT         -> server template/sanitizer
CSRF                             -> framework + server policy
STRICT CSP                       -> Alpine CSP strategy
EXPECTED VALIDATION              -> usable form HTML
NETWORK FAILURE                  -> retry/ambiguity policy
GLOBAL LISTENER                  -> narrow + cross-cutting
MIXED COMPONENT                  -> written contract
MIGRATION                        -> pin, audit, test defaults
```

## Exit exercise

Perform a production readiness review for a bulk financial approval workflow. Include authorization, CSRF, idempotency, conflict handling, OOB totals, success fact, focus, retry ambiguity, audit logging, direct routes, lifecycle cleanup, and htmx 4 migration risk.

## Exit criteria

You pass when another engineer can operate, review, test, and migrate the feature from the written contract without relying on tribal knowledge.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---

# Day 12: Power-User Showcase — Pragmatic Systems That Feel Slightly Impossible

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Serve as a teaser for the unexpected power of disciplined Alpine and htmx composition. The examples are intentionally ambitious, but remain business-oriented, server-authoritative, progressively enhanced, and explainable.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- compose many small, independently owned capabilities into one sophisticated workflow;
- build interfaces that feel application-like without a global client entity store;
- combine local-first interaction, server-settled truth, OOB projections, semantic events, history, polling, and JavaScript islands;
- recognize when a “bonkers” effect is actually several simple contracts;
- preserve debuggability while increasing power.

## 1. The paradox

The most surprising Alpine + htmx systems are not powered by one giant abstraction. They are powered by strict limits:

- server owns durable truth;
- htmx owns server communication and HTML installation;
- Alpine owns temporary local interaction;
- URLs own meaningful locations;
- controls carry submitted values;
- OOB swaps update related server projections;
- semantic events request or announce;
- plain JavaScript owns specialized rendering;
- every DOM region has one writer.

The “magic” is composition.

## 2. Showcase A: server-backed command palette

Imagine pressing `Ctrl+K` anywhere in an operations application.

Alpine owns:

- open/closed state;
- keyboard shortcut;
- active result index;
- focus;
- local query draft;
- selection highlight.

htmx owns:

- debounced server search;
- cancellation of stale requests;
- installation of result HTML.

Server owns:

- permissions;
- command availability;
- result ranking;
- route destinations.

URL owns the destination after activation.

### Stable shell

```html
<section
    x-data="commandPalette"
    @keydown.ctrl.k.window.prevent="open"
    @command-palette-close.window="close"
>
    <dialog x-ref="dialog" aria-labelledby="command-title">
        <h2 id="command-title">Command palette</h2>

        <form
            action="/commands"
            method="get"
            hx-get="/commands"
            hx-trigger="input changed delay:150ms from:#command-query"
            hx-target="#command-results"
            hx-sync="this:replace"
        >
            <input
                id="command-query"
                name="q"
                x-model="query"
                x-ref="query"
                autocomplete="off"
            >
        </form>

        <div
            id="command-results"
            @keydown.arrow-down.prevent="move(1)"
            @keydown.arrow-up.prevent="move(-1)"
            @keydown.enter.prevent="activate"
        >
            <!-- server-rendered, permission-filtered commands -->
        </div>
    </dialog>
</section>
```

The server returns links with real `href` values. htmx may boost navigation. Alpine never owns the command catalog.

### Why it feels wild

The palette has application-grade keyboard behavior and instant local presentation, but every result remains a server-authorized hypermedia control.

## 3. Showcase B: bulk-action transaction cockpit

A user selects 300 visible records, previews impact, confirms, submits one command, receives server validation, and sees five related projections update.

Alpine owns:

- temporary selection;
- toolbar visibility;
- local estimated count;
- confirmation dialog;
- reversible highlight.

Native controls own:

- submitted selected IDs;
- selected action;
- confirmation token.

htmx owns:

- submission;
- disabling;
- synchronization;
- primary replacement.

Server owns:

- permission per record;
- final eligible set;
- transaction;
- conflicts;
- audit log;
- canonical results.

OOB swaps update:

- result table;
- selected segment summary;
- outstanding total;
- audit activity count.

A fact event:

- closes confirmation;
- clears local selection;
- shows toast.

### Control bridge

```html
<section x-data="bulkSelection">
    <form
        id="bulk-action-form"
        action="/expenses/bulk"
        method="post"
        hx-post="/expenses/bulk"
        hx-target="#expense-results"
        hx-swap="outerHTML"
        hx-disabled-elt="find fieldset, find button[type='submit']"
        hx-sync="this:replace"
    >
        <fieldset>
            <input
                type="hidden"
                name="selected_ids"
                :value="selectedIds.join(',')"
            >

            <select name="action">
                <option value="approve">Approve</option>
                <option value="return">Return</option>
            </select>
        </fieldset>

        <button
            type="button"
            @click="openConfirmation"
            :disabled="selectedIds.length === 0"
        >
            Review <span x-text="selectedIds.length"></span> selected
        </button>

        <button
            x-ref="submit"
            type="submit"
            hidden
        >
            Submit
        </button>
    </form>

    <!-- local confirmation UI -->
</section>
```

Before dispatching the command, Alpine ensures the hidden control is current. The server ignores the client’s estimated eligibility and recalculates.

Response:

```html
<section id="expense-results">
    <!-- canonical rows and validation/conflict messages -->
</section>

<section id="expense-summary" hx-swap-oob="outerHTML">
    <!-- canonical totals -->
</section>

<span id="pending-count" hx-swap-oob="outerHTML">18</span>

<section id="audit-preview" hx-swap-oob="outerHTML">
    <!-- latest related audit entries -->
</section>
```

Header:

```http
HX-Trigger-After-Swap: {"bulk-action-completed":{"processed":282,"rejected":18}}
```

This feels like a client transaction engine. It is actually native controls, one server transaction, returned HTML, OOB projections, and one fact.

## 4. Showcase C: live incident command center

The page shows:

- live incident state;
- responders;
- timeline;
- command forms;
- local panel layout;
- a high-frequency telemetry chart;
- alerts;
- route-backed incident selection.

Ownership map:

| Capability | Owner |
| --- | --- |
| incident truth | server |
| route `/incidents/42` | URL/server |
| timeline HTML | htmx SSE or polling |
| responder count | OOB |
| local panel expansion | Alpine |
| muted alert preference | Alpine/browser storage |
| telemetry drawing | JavaScript island |
| telemetry snapshot label | server/htmx |
| toast presentation | Alpine |
| command authorization | server |

### Stable shell with multiple interiors

```html
<main x-data="{ timelineOpen: true, detailsOpen: true }">
    <section id="incident-status">
        <!-- primary server state -->
    </section>

    <aside x-cloak x-show="timelineOpen">
        <div id="incident-timeline">
            <!-- streamed/polled server HTML -->
        </div>
    </aside>

    <section x-ignore id="telemetry-chart">
        <!-- specialized renderer -->
    </section>

    <section x-cloak x-show="detailsOpen">
        <div id="incident-details"></div>
    </section>
</main>
```

One server event can update status and responder count, emit an alert fact, and send low-frequency chart metadata. The chart’s high-frequency samples use a specialized channel and renderer.

### Why it feels wild

The interface is live, stateful, navigable, and multi-region, yet no single client store tries to reconcile the whole incident.

## 5. Showcase D: optimistic-enough inventory allocation

A warehouse planner drags provisional allocations between orders.

Local behavior:

- drag ghost;
- keyboard reorder;
- provisional totals;
- conflict highlighting;
- reversible layout.

Submission:

- hidden named controls contain proposed order;
- htmx submits the completed proposal;
- `hx-sync` prevents contradictory overlapping proposals.

Server:

- validates inventory, locks, priorities, permissions;
- returns canonical allocation;
- identifies rejected moves;
- emits `allocation-settled`.

Flow:

```text
Alpine provisional move
    -> named controls updated
        -> allocation-commit-requested
            -> htmx POST
                -> server transaction
                    -> canonical board HTML
                    -> OOB capacity totals
                    -> allocation-settled fact
                        -> Alpine clears provisional markers
```

This is optimism without transferring authority.

Requirements:

- provisional state is visually distinct;
- keyboard alternative exists;
- rollback is automatic through returned HTML;
- repeated activation is controlled;
- conflict response explains rejected moves;
- no silent assumption that the server accepted the drag.

## 6. Showcase E: self-reconfiguring workflow console

A multi-step server workflow can return a different component shape at every step:

1. search and select a customer;
2. reveal local comparison controls;
3. submit a server choice;
4. return an authorization challenge;
5. poll a background calculation;
6. display a review form;
7. complete and update several projections.

A stable Alpine shell may retain:

- current panel visibility;
- help disclosure;
- local density;
- focus policy.

The htmx interior may transform from form to challenge to progress to review to result.

No client state machine must mirror every server step. The installed HTML is the workflow’s current representation.

## 7. The capstone: “one-page operations brain”

Build a page containing:

- route-backed record explorer;
- `Ctrl+K` command palette;
- active search with cancellation;
- bulk local selection;
- server-settled bulk action;
- remote dialog validation;
- OOB totals and counts;
- toast center;
- polling export job;
- isolated chart;
- history and direct routes;
- keyboard and focus policy;
- no global entity store.

The point is not to maximize attributes. The point is to keep every feature a small contract.

## 8. Capstone architecture map

```text
URL
  /operations?q=late&segment=enterprise&record=42

SERVER PAGE
  canonical route, permissions, complete HTML

ALPINE SHELLS
  command palette
  filter disclosure
  bulk selection
  remote dialog
  toast center
  local layout preferences

HTMX TARGETS
  #operation-results
  #record-details
  #dialog-body
  #export-job
  #incident-timeline

OOB TARGETS
  #result-count
  #financial-summary
  #unread-count

JS ISLANDS
  #telemetry-chart

EVENTS
  command-palette-close
  bulk-action-completed
  record-saved
  export-completed
```

No arrow should point to two authoritative owners for the same state.

## 9. “Bonkers” composition rules

### Rule 1: multiply capabilities, not authorities

Ten small components are safer than one universal store.

### Rule 2: returned HTML is a state machine representation

A server workflow can advance by returning the next complete component.

### Rule 3: local shells can outlive many server representations

A dialog, drawer, palette, or workspace can remain stable while its interior changes repeatedly.

### Rule 4: one transaction may have several authoritative projections

Use bounded OOB swaps.

### Rule 5: one fact may have several local listeners

A saved record may close a dialog and add a toast, provided listeners own only presentation.

### Rule 6: specialized islands are allowed

A chart or editor does not require converting the whole product into a client SPA.

### Rule 7: history keeps the system honest

Every meaningful location still works directly.

## 10. Failure containment

A sophisticated composition should degrade locally.

- command search failure does not destroy page state;
- bulk validation returns usable rows and messages;
- toast failure does not conceal the canonical result;
- chart failure does not remove server status text;
- polling failure exposes retry;
- modal validation does not close the modal;
- history restore can request the server;
- one widget teardown does not leak across pages.

Avoid a single global event bus or store whose failure disables the product.

## 11. Performance reasoning

Use the smallest proportionate mechanism:

- local show/hide: Alpine;
- server list: htmx fragment;
- related count: OOB;
- 2-second status: polling;
- sparse server pushes: SSE;
- high-frequency graphics: specialized renderer;
- navigation: URL;
- large client algorithm: isolated module.

Measure before adding morphing, preservation, prefetch, or streaming.

## 12. Power-user review questions

Before approving an advanced feature:

1. Can every state value name one authority?
2. Can every region name one writer?
3. Does every request have a route, target, response root, and race policy?
4. Does every event have one documented meaning?
5. Are OOB updates directly related?
6. Does every Alpine shell have a refresh test?
7. Does every external resource have cleanup?
8. Do direct URLs work?
9. Is focus predictable?
10. Can the server reject every optimistic assumption?
11. Can one failed component degrade independently?
12. Is a JavaScript island isolated rather than contagious?

## 13. Day lab: build the impossible-looking demo

Implement a reduced “operations brain” with:

- command palette;
- active search;
- selected rows;
- bulk approve;
- updated summary via OOB;
- remote validation dialog;
- success toast;
- export job polling;
- one simulated chart island.

Required proof:

- disable JavaScript and use the core search and submit routes;
- throttle network and demonstrate race safety;
- force validation and conflict;
- navigate Back/Forward;
- replace components repeatedly and inspect cleanup;
- show that no Alpine store contains server entities;
- show that each pushed URL works directly.

## Mini-tasks

### Task A: decompose apparent magic

Take a flashy workflow and list the small contracts that create it.

### Task B: reject authority creep

Find where a proposed global store duplicates server truth.

### Task C: choose transport

Choose polling, SSE, normal request, OOB, event, or JavaScript island for ten example updates.

### Task D: chaos test

Inject slow, failed, duplicated, and out-of-order responses. Record which owner restores correctness.

## Comprehension check

1. Why can a server-rendered workflow avoid a client state machine?
2. What makes a stable shell powerful?
3. How can one transaction update many projections without a client store?
4. What is “optimistic enough”?
5. Why should event facts remain small?
6. When is a JavaScript island the right escalation?
7. What role does history play in advanced systems?
8. Why is failure containment easier with explicit boundaries?
9. What creates the appearance of “magic”?
10. What is the non-negotiable rule at power-user scale?

## Cheat sheet

```text
COMPLEX UX != ONE COMPLEX OWNER

stable local shell
+ authoritative server interior
+ real controls
+ race-safe requests
+ bounded OOB projections
+ semantic facts
+ route-backed state
+ isolated specialized widgets
= application-grade behavior without duplicated authority
```

## Exit exercise

Write the architecture for a “financial close command center” that supports route-backed period selection, live job status, local comparison tools, remote approval dialogs, bulk actions, OOB financial totals, auditable server outcomes, keyboard command palette, and a specialized chart. Make it look outrageous. Keep every contract boring.

## Exit criteria

You complete the series when you can design an interface that feels far more dynamic than its architecture is complicated—and explain every line by state owner, DOM writer, request contract, lifecycle, event, and failure path.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.


---
