Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when adding, changing, or debugging Alpine.js behavior in Django-rendered templates, especially when Alpine.js coexists with HTMX partial updates.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 58% | 0% |
| case-09 | ✓→✓ | = Same ✓ | 40% | 0% |
Use this skill for local browser state in Django-rendered templates: dropdowns, modals, tabs, disclosures, inline filters, disabled/loading states, small preview interactions, and client-only toggles. Use Django views/forms/models as the source of truth for durable state. Use HTMX when the server needs to return fresh HTML.
bundling pipeline as the source of truth.
Alpine, and app JavaScript. Preserve the current ordering unless changing initialization deliberately.
not call Alpine.start() when the project already loads a self-starting Alpine build. If using a module build, register components and start Alpine from the owning entrypoint.
x-data.
Alpine starts. Check the existing script order before relying on document.addEventListener("alpine:init", ...) from an app-wide script.
entrypoint that runs before Alpine starts. Register components and stores inside document.addEventListener("alpine:init", ...), include the file through the project's normal asset path, and avoid double-starting Alpine.
HTML.
is not naturally scoped to one Alpine component.
Use a small x-data object near the markup it controls:
html<div x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false" > <button type="button" :aria-expanded="open.toString()" @click.stop="open = !open" > Menu </button> <div x-cloak x-show="open" x-transition> ... </div> </div>
Prefer Alpine directives over manual DOM manipulation:
x-show for toggling visibility while keeping the element in the DOM.x-if on a <template> when the element should be created and destroyed.x-model for client-only input state; Django form submission and validationstill happen on the server.
:class, :disabled, :aria-expanded, and :hidden for state-derivedattributes.
$watch for one named state transition; x-effect only when the dependencyset is simple and intentional.
$dispatch for browser events between Alpine components or from Alpine toHTMX triggers.
Use x-cloak for anything hidden by default. Ensure the base CSS includes the required [x-cloak] { display: none !important; } rule.
Keep Django interpolation out of complex JavaScript expressions when possible.
For simple values, prefer HTML attributes:
html<div data-initial-label="{{ object.name }}" x-data="{ label: '' }" x-init="label = $el.dataset.initialLabel" > <span x-text="label"></span> </div>
For structured values, prefer Django's json_script and parse it in x-init:
html{{ rows|json_script:"rows-data" }} <div x-data="{ rows: [] }" x-init="rows = JSON.parse(document.getElementById('rows-data').textContent)" > <template x-for="row in rows" :key="row.id"> <span x-text="row.name"></span> </template> </div>
Avoid |safe inside Alpine expressions. If the browser must display text, use x-text. Use x-html only for trusted, already-sanitized HTML; most HTML should be rendered by Django or returned through HTMX.
When HTMX swaps a fragment containing x-data, Alpine should initialize the new component automatically. Do not restart Alpine after HTMX swaps.
Keep persistent Alpine state outside HTMX targets that will be replaced:
html<section x-data="{ panelOpen: false }"> <button type="button" @click="panelOpen = !panelOpen">Filters</button> <div id="results" hx-get="{% url 'search_results' %}" hx-trigger="change from:#filters"> ... </div> </section>
Use events as the boundary between HTMX responses and Alpine state. From Django, set an HX-Trigger or HX-Trigger-After-Swap response header, then listen from Alpine:
html<div x-data="{ open: true }" @profile-saved.window="open = false"> <form hx-post="{% url 'profile_update' %}" hx-target="#profile-panel" hx-swap="outerHTML"> ... </form> </div>
Listen for HTMX lifecycle events in kebab case from Alpine, such as @htmx:after-swap.window, because HTML attributes are case-insensitive.
If Alpine dispatches an event that HTMX should react to, make the trigger explicit:
html<button type="button" x-data @click="$dispatch('refresh-results')"> Refresh </button> <div hx-get="{% url 'results' %}" hx-trigger="refresh-results from:body"> ... </div>
<button type="button"> unless submitting a form.aria-expanded,aria-controls, aria-selected, and aria-hidden.
@keydown.escape.window for dismissible overlays.@click.outside for popovers and menus, paired with a visible trigger.trapping is needed, add a focused plain JS module or Alpine plugin deliberately instead of hand-rolling a brittle trap in attributes.
x-data,data-*, json_script, or hidden inputs unless the user is allowed to see them.
unsafe-eval is required, switchdeliberately to Alpine's CSP build and retest Alpine expressions. The default alpinejs/dist/cdn.min.js build is not the CSP build.
fetch() requests, include Django CSRF headers or useexisting HTMX/Django forms instead.
cover the page, plus npm run lint if JavaScript modules changed.
output.
UI behavior.
Other measured skills in the registry, with their headline benchmark lift.