# Alpine.js for Developers Who Already Know htmx

## Purpose

This guide teaches Alpine.js by starting from an htmx-shaped mental model.

> **Version baseline:** The examples target Alpine.js 3.x and stable htmx 2.x. htmx 4 is currently a prerelease line with changed event names, inheritance defaults, and error-swapping behavior; verify its migration guide before applying these examples to htmx 4.

You already know how to:

- put behavior in HTML;
- submit real forms and follow real links;
- ask the server for HTML;
- replace a deliberate DOM target;
- let the server remain authoritative.

Alpine adds a different capability: **small, immediate, reactive behavior that belongs entirely to the current browser interface**.

The useful division is:

| Need | Default owner |
| --- | --- |
| Ask the server for authoritative HTML | htmx |
| Change durable application state | Server, reached through htmx |
| Open a menu immediately | Alpine |
| Track which local tab is selected | Alpine |
| Bind a draft value to an input | Alpine plus the native control |
| Show request progress | htmx request state plus CSS |
| Maintain bookmarkable state | URL and server route |
| Perform a substantial browser algorithm | Plain JavaScript module |

The key idea is not “use Alpine instead of htmx.” It is “stop making server round trips for behavior that is temporary, immediate, and local.”

---

## 1. The Alpine mental model

An Alpine component is a DOM subtree with a small reactive state object.

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

    <section x-show="open">
        Local details
    </section>
</div>
```

Read this as:

1. `x-data` establishes a component boundary.
2. `open` is browser-local state.
3. `@click` changes that state immediately.
4. `x-show` reflects the state in the DOM.

Nothing is fetched. Nothing durable changes. A refresh may reset `open` without corrupting the application.

That refresh test is useful:

> When losing a value on refresh would make the application incorrect, Alpine must not be its only authority.

Good Alpine state includes:

- dropdown visibility;
- modal visibility;
- selected local tab;
- keyboard focus index;
- temporary draft text;
- provisional calculations;
- a toast queue;
- a browser-only preference.

Poor default Alpine state includes:

- permissions;
- cart truth;
- prices;
- inventory;
- saved records;
- workflow status;
- server validation truth;
- application routing.

---

## 2. Installation and initialization

A typical server-rendered page can load both libraries as ordinary scripts.

```html
<script src="/vendor/htmx.min.js" defer></script>
<script src="/vendor/alpine.min.js" defer></script>
```

Use self-hosted or pinned assets according to the project’s dependency policy.

Add an `x-cloak` rule so initially hidden Alpine content does not flash before Alpine initializes.

```css
[x-cloak] {
    display: none !important;
}
```

```html
<div x-data="{ open: false }">
    <div x-cloak x-show="open">
        Hidden until Alpine is ready and `open` is true.
    </div>
</div>
```

Alpine initializes `x-*` markup already in the document and also initializes Alpine markup introduced by htmx swaps. Treat each swapped-in Alpine component as a fresh instance unless you deliberately keep its root outside the htmx target.

---

## 3. Translating htmx instincts into Alpine instincts

### htmx asks “what request should this element make?”

Alpine asks “what local state should this element change?”

```html
<!-- htmx: server interaction -->
<button
    hx-post="/cart/items/42"
    hx-target="#cart"
    hx-swap="outerHTML"
>
    Add to cart
</button>

<!-- Alpine: local interaction -->
<div x-data="{ open: false }">
    <button type="button" @click="open = true">
        Open cart drawer
    </button>
</div>
```

A combined button can do both, but ownership should remain visible.

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

The server can emit a semantic event after success, and Alpine can open the drawer in response. The request remains htmx-owned; the drawer remains Alpine-owned.

### `hx-target` defines a server replacement boundary

`x-data` defines a local state boundary.

These boundaries may nest, but they should not accidentally claim the same descendants.

Preferred mixed shape:

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

    <div x-show="open" x-transition>
        <div id="cart-contents">
            <!-- htmx replaces this inner server-owned region -->
        </div>
    </div>
</aside>
```

The Alpine shell survives. htmx replaces only `#cart-contents`.

---

## 4. Core directives

### `x-data`: declare local state and methods

Inline state is appropriate when it stays small.

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

Use a registered component when behavior is reused or becomes substantial.

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

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

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

<div x-data="disclosure">
    <button type="button" @click="toggle">Toggle</button>
    <section x-show="open">Content</section>
</div>
```

A useful rule: attributes should declare behavior, not contain an entire implementation.

### `x-on` and `@event`: handle browser events

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

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

Useful modifiers include:

- `.window` for events dispatched on `window`;
- `.outside` for clicks outside an element;
- `.prevent` for `preventDefault()`;
- `.stop` for `stopPropagation()`;
- keyboard modifiers such as `.escape` and `.enter`.

Use Alpine events for immediate browser interaction. Do not reproduce htmx request, target, swap, or synchronization logic inside `@click` handlers.

### `x-show`: toggle visibility without destroying the subtree

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

Choose `x-show` when:

- state inside the subtree should survive hiding;
- focusable controls will be reused;
- transitions matter;
- htmx attributes inside the subtree were already processed.

### `x-if`: insert and remove a subtree

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

    <template x-if="mounted">
        <section>Fresh subtree each time</section>
    </template>
</div>
```

Choose `x-if` when the subtree should genuinely cease to exist.

Important htmx integration detail: when Alpine creates new markup containing `hx-*` attributes, htmx may need to process that new subtree explicitly.

```html
<div
    x-data="{ mounted: false }"
    x-init="$watch('mounted', value => {
        if (value) {
            $nextTick(() => htmx.process(document.getElementById('remote-panel-region')))
        }
    })"
>
    <button type="button" @click="mounted = true">
        Mount remote panel
    </button>

    <template x-if="mounted">
        <section id="remote-panel-region">
            <button
                hx-get="/panel"
                hx-target="#panel-body"
            >
                Load panel
            </button>
            <div id="panel-body"></div>
        </section>
    </template>
</div>
```

Process only the newly created region, not the entire document.

### `x-bind` and `:`: 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 include:

- `aria-expanded`;
- `aria-selected`;
- `disabled` for local conditions;
- classes derived from local state;
- local `data-*` attributes.

Do not bind durable permissions or prices from a long-lived Alpine copy when the server can render them authoritatively.

### `x-text`: show local text safely

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

Use it for counts, labels, provisional values, and local status text.

### `x-html`: insert trusted HTML only

`x-html` is rarely needed in a server-rendered htmx application. When the desired result is server-generated interface HTML, let htmx obtain and install it through a declared target.

Never insert untrusted HTML without a deliberate sanitization policy.

### `x-model`: connect Alpine state to a real form control

This is one of the strongest Alpine–htmx bridges.

```html
<form
    x-data="{ quantity: 1 }"
    hx-post="/cart/quantity"
    hx-target="#cart-line-42"
    hx-swap="outerHTML"
>
    <label>
        Quantity
        <input
            type="number"
            name="quantity"
            min="1"
            x-model.number="quantity"
        >
    </label>

    <p>Provisional quantity: <span x-text="quantity"></span></p>

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

The ownership chain is clear:

- the input owns the submitted value;
- Alpine observes it and provides immediate feedback;
- htmx submits it;
- the server validates and returns the canonical result.

Avoid keeping a server-required value only in an Alpine property that is never represented by a named control or explicit request parameter.

### `x-for`: render small browser-owned collections

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

Good uses:

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

A large `x-for` rendering database records is a warning that Alpine is becoming a second client rendering system. Server-owned collections should normally arrive as server-rendered HTML through htmx.

### `x-transition`: animate local visibility

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

Use Alpine transitions for Alpine-controlled visibility. Use htmx swap classes and swap timing for htmx-controlled replacement. Do not make both systems independently animate the same element’s removal.

---

## 5. Component methods, computed values, and reactivity

Alpine expressions can call methods and getters on the component object.

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

This total is a preview. The server still owns tax, discounts, inventory, and the accepted transaction amount.

Use `x-effect` when a local side effect should rerun as its dependencies change.

```html
<div x-data="{ open: false }" x-effect="document.body.inert = open">
    <!-- simplified example; real modal behavior requires careful focus handling -->
</div>
```

Use `$watch` when you need old and new values or want to watch one declared path.

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

Do not turn effects into a hidden network layer. When a local change should cause a server request, make that contract explicit with an event and let htmx own request timing.

---

## 6. DOM references and timing

Use `x-ref` and `$refs` 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` when Alpine must finish updating its own DOM before you measure, focus, or process a newly inserted subtree.

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

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

Do not confuse `$nextTick` with htmx lifecycle events:

- `$nextTick` means Alpine finished its local reactive update;
- `htmx:afterSwap` means htmx installed response content;
- `htmx:afterSettle` means htmx completed settling work.

---

## 7. Lifecycle: `init()` and `destroy()`

An extracted component can initialize and clean up external resources.

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

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

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

Cleanup matters because an htmx swap may remove an Alpine component.

Clean up:

- timers;
- global event listeners;
- observers;
- media-query listeners;
- third-party widgets;
- browser API handles;
- subscriptions.

Initialization should be safe to repeat because server responses may create fresh component instances many times.

---

## 8. Alpine stores

A store is shared reactive browser state.

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

Appropriate store contents:

- theme;
- global drawer visibility;
- command palette state;
- toast presentation;
- a small client-only preference.

Inappropriate default contents:

- normalized server entities;
- permissions;
- cart truth;
- routing;
- workflow state;
- an independent cache of the application.

Default to local `x-data`. Reach for a store only when several independent components genuinely share browser-interface state.

---

## 9. Additional Alpine tools worth knowing

These are not required for the first component, but they become useful in mature Alpine–htmx systems.

### `x-modelable`

Use `x-modelable` to expose a reusable Alpine-local control value to a parent component. When the server needs that value, bind or mirror it to a real named form control before htmx submits the form.

### `x-teleport`

Use teleportation for overlays that must escape stacking or clipping contexts. Put the teleport destination outside frequently replaced htmx targets so the overlay shell is not destroyed by unrelated swaps.

### `x-ignore`

Use `x-ignore` to keep Alpine from walking into a third-party widget or deliberately isolated subtree. It is an isolation boundary, not a repair for confused component ownership.

### `x-id` and `$id`

Use Alpine-generated IDs for local accessibility relationships such as tabs, labels, and descriptions. Do not confuse those IDs with stable server IDs required by htmx targets, OOB swaps, or `hx-preserve`.

### `Alpine.bind()`

Use `Alpine.bind()` for reusable groups of Alpine attributes and event declarations. Keep important htmx request contracts visible in the markup; do not hide every endpoint, target, and trigger behind a magical binding package.

### Plugins

Adopt plugins for demonstrated needs rather than by default:

| Plugin | Appropriate role beside htmx |
| --- | --- |
| Focus | Local focus trapping and restoration |
| Persist | Non-authoritative browser preferences |
| Intersect | Local viewport reactions; use htmx triggers when the only goal is loading HTML |
| Resize | Measured local behavior CSS cannot express |
| Mask | Input presentation; the server still validates and normalizes |
| Collapse | Local disclosure animation |
| Anchor | Local popover positioning |
| Morph | Exceptional state-preserving reconciliation of server HTML |
| Sort | Local drag mechanics; htmx submits and the server settles canonical order |

Morphing and persistence should not compensate for unclear ownership.

---

## 10. Events: Alpine’s bridge to htmx

Use `$dispatch` to announce a semantic browser event.

```html
<div x-data>
    <button
        type="button"
        @click="$dispatch('delete-confirmed', { id: 42 })"
    >
        Confirm delete
    </button>
</div>
```

An htmx element can listen for that event.

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

Use event names that describe the domain action:

- `delete-confirmed`;
- `cart-updated`;
- `profile-saved`;
- `dialog-close-requested`.

Avoid names that expose implementation details:

- `call-htmx-button`;
- `set-store-open`;
- `swap-right-div`.

For htmx-to-Alpine communication, let the server emit a response-triggered event.

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

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

Choose timing intentionally:

- immediate response event when the DOM does not matter;
- after swap when Alpine needs the new content present;
- after settle when focus or transition timing requires the final DOM.

---

## 11. Four component shapes to recognize

### Alpine-only local component

Use for a dropdown, disclosure, local tabs, theme control, or character counter.

### htmx-only server component

Use for pagination, a delete action, a refresh panel, or a straightforward server form.

### Stable Alpine shell with an htmx interior

Use when local state should survive server updates.

Examples:

- open cart drawer with refreshed cart contents;
- open modal with a replaced validation form;
- filter disclosure with replaced results.

### htmx-replaceable component containing disposable Alpine behavior

Use when a server response should reset local state.

Examples:

- inline editor replaced by a saved record;
- form replaced by confirmation;
- row menu discarded when the row is rerendered.

Before every target choice, decide whether Alpine state should reset or survive.

---

## 12. Common mistakes from an htmx-first perspective

### Making a request for local trivia

Opening a menu, switching already-present tabs, or showing a preview does not need a server round trip.

### Replacing an Alpine root that was expected to survive

Shrink the htmx target or move the Alpine shell outside it.

### Creating an Alpine `loading` flag for every htmx request

Prefer `hx-indicator`, `hx-disabled-elt`, the `htmx-request` class, and htmx lifecycle events.

### Using Alpine to interpret arbitrary JSON into UI

When the result is interface HTML, let the server render it and htmx install it.

### Putting all local state in a global store

Local state should stay local. A global store is not a requirement for component communication; browser events often provide looser coupling.

### Letting Alpine and htmx render the same list

Choose one writer. Server-owned lists normally belong to server templates and htmx swaps.

---

## 13. A practical decision sequence

For each behavior, ask:

1. Can semantic HTML do it?
2. Can CSS do it without application state?
3. Is it a specialized algorithm or browser API that belongs in plain JavaScript?
4. Does it require the server or change authoritative state? Use htmx.
5. Is it immediate, temporary, and local? Use Alpine.
6. Does it need both? Create separate Alpine and htmx boundaries and connect them with native controls or semantic events.
7. Do both appear to own the same state or descendants? Redesign the boundary.

---

## 14. Compact Alpine checklist for htmx developers

Before adding Alpine:

- The state is safe to discard or deliberately persisted as a browser preference.
- No server rule depends on an unsubmitted Alpine-only value.
- The component has a clear `x-data` boundary.
- Local state is minimal.
- Inline expressions remain readable.
- External resources have cleanup.
- Focus and keyboard behavior are defined.
- The htmx target will either preserve or intentionally replace the Alpine root.
- Newly Alpine-created `hx-*` markup is processed when necessary.
- Cross-boundary actions use semantic events.

---

## 15. Where to continue

Read the companion guides in this order when building a mixed system:

1. `htmx-for-alpine-developers.md` for the inverse mental model.
2. `alpine-and-htmx-as-coequals.md` for architectural rules and component contracts.
3. `alpine-htmx-superpowers.md` for advanced combined patterns.

The durable mental model is simple:

> Use Alpine for what is happening locally now. Use htmx for what the server knows or changes. Keep the DOM boundary between them intentional.
