name: alpine-development description: Build interactive UIs with Alpine.js and Alpine AJAX, including reactive components, AJAX requests, and dynamic content updates.
Alpine.js Development
Build reactive, declarative user interfaces using Alpine.js (v3.x) and Alpine AJAX for asynchronous content updates.
When to use this skill
Use this skill when:
- Creating or modifying Alpine.js components
- Working with Alpine directives (x-data, x-show, x-if, x-on, x-model, x-bind, x-for, x-ref, etc.)
- Implementing AJAX functionality with Alpine AJAX (x-target, x-merge, x-headers)
- Adding client-side interactivity to Blade components
- Building dynamic forms, dropdowns, modals, or toggles
- Handling asynchronous HTTP requests without page reloads
- The user mentions Alpine, Alpine AJAX, or client-side interactivity
Architecture Context
This project uses Blade + Alpine.js + Alpine AJAX as the primary frontend stack for the entire application. Authentication is built with Laravel Fortify using custom Blade + Alpine.js UI (not Livewire).
Core Alpine.js Concepts
Declaring Component State with x-data
The x-data directive defines a reactive data scope for a component:
<div x-data="{ open: false, count: 0 }">
<!-- Component content with access to 'open' and 'count' -->
</div>
Event Handling with x-on (@)
Listen to DOM events using x-on: or the @ shorthand:
<button @click="count++">Increment</button>
<button x-on:click="open = !open">Toggle</button>
<div @click.outside="open = false">Closes when clicking outside</div>
Common event modifiers:
.prevent- preventDefault().stop- stopPropagation().outside- trigger when clicking outside element.window- listen on window object.once- remove listener after first invocation
Displaying Data with x-text and x-html
<span x-text="count"></span>
<div x-html="rawHtmlContent"></div>
Two-Way Data Binding with x-model
Bind input values to Alpine data:
<div x-data="{ message: '' }">
<input type="text" x-model="message">
<span x-text="message"></span>
</div>
Conditional Rendering
x-show - Toggle visibility with CSS (element stays in DOM):
<div x-show="open">Content...</div>
x-if - Add/remove from DOM (must be on <template> tag):
<template x-if="open">
<div>Content...</div>
</template>
Looping with x-for
Iterate over arrays (must be on <template> tag):
<div x-data="{ items: ['apple', 'banana', 'orange'] }">
<template x-for="item in items">
<li x-text="item"></li>
</template>
</div>
Component References with x-ref
Access DOM elements directly:
<div x-data>
<input x-ref="emailInput" type="email">
<button @click="$refs.emailInput.focus()">Focus Email</button>
</div>
Alpine AJAX
Alpine AJAX extends Alpine.js with directives for making HTTP requests and updating page content.
Basic AJAX with x-target
The x-target directive specifies which element(s) to update with the response:
<ul id="comments">
<li>Comment #1</li>
</ul>
<form x-target="comments" method="post" action="/comment">
<input name="text" required/>
<button>Submit</button>
</form>
Multiple targets:
<h2>Comments (<span id="comments_count">1</span>)</h2>
<ul id="comments">
<li>Comment #1</li>
</ul>
<form x-target="comments comments_count" method="post" action="/comment">
<input name="comment" required/>
<button>Submit</button>
</form>
AJAX Links
Links can also make AJAX requests:
<a href="/contacts/1/edit" x-target="contact_1">Edit</a>
Custom Headers with x-headers
Add custom HTTP headers to requests:
<form method="post" action="/comments"
x-target="comments"
x-headers="{'Custom-Header': 'Value'}">
<!-- form fields -->
</form>
Alpine AJAX automatically includes these headers:
X-Alpine-Request: true- Identifies Alpine AJAX requestsX-Alpine-Target: {target-ids}- Lists target element IDs
Merge Strategies with x-merge
Control how response content replaces existing content:
Replace (default) - Replace entire element:
<div id="content" x-target>
<!-- Will be completely replaced -->
</div>
Append - Add new content to the end:
<ul id="messages" x-merge="append">
<li>First message</li>
<!-- New messages appended here -->
</ul>
Prepend - Add new content to the beginning:
<ul id="notifications" x-merge="prepend">
<!-- New notifications prepended here -->
<li>Old notification</li>
</ul>
Morph - Intelligently merge content (requires @alpinejs/morph plugin):
<div id="contacts" x-merge="morph">
<!-- Form state and attributes preserved during update -->
</div>
The morph strategy preserves:
- Input focus states
- Form values
- Scroll positions
- Selected elements
Install morph plugin:
npm install @alpinejs/morph
Inline Edit Pattern
Dynamically switch between view and edit modes:
<div id="contact_1" x-merge.transition>
<p><strong>Name:</strong> John Doe</p>
<p><strong>Email:</strong> john@example.com</p>
<a href="/contacts/1/edit" x-target="contact_1">Edit</a>
</div>
When the edit link is clicked, server returns the edit form:
<form id="contact_1" x-target x-merge.transition method="put" action="/contacts/1">
<input name="first_name" value="John">
<input name="email" value="john@example.com">
<button>Update</button>
<a href="/contacts/1" x-target="contact_1">Cancel</a>
</form>
Global State with Alpine.store
Share state across multiple components:
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', {
on: false,
toggle() {
this.on = !this.on
}
})
})
Access in components:
<div x-data>
<button @click="$store.darkMode.toggle()">Toggle Dark Mode</button>
<div x-show="$store.darkMode.on">Dark mode content</div>
</div>
Component Communication with $dispatch
Send custom events between components:
<div x-data="{ title: 'Hello' }"
@set-title.window="title = $event.detail">
<h1 x-text="title"></h1>
</div>
<div x-data>
<button @click="$dispatch('set-title', 'Hello World!')">
Update Title
</button>
</div>
No Livewire
Important: This project does NOT use Livewire at all. Authentication is built with Laravel Fortify using custom Blade + Alpine.js UI.
All interactivity across the entire application (public site and admin panel) uses:
- Blade templates for rendering
- Alpine.js for client-side reactivity
- Alpine AJAX for server communication
Common Patterns
Dropdown Menu
<div x-data="{ open: false }">
<button @click="open = !open">Menu</button>
<div x-show="open" @click.outside="open = false">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
</div>
</div>
Modal Dialog
<div x-data="{ showModal: false }">
<button @click="showModal = true">Open Modal</button>
<template x-if="showModal">
<div @click.self="showModal = false" class="modal-backdrop">
<div class="modal-content">
<h2>Modal Title</h2>
<button @click="showModal = false">Close</button>
</div>
</div>
</template>
</div>
Infinite Scroll with AJAX
<table>
<tbody id="records" x-merge="append">
<tr>
<td>Record 1</td>
</tr>
</tbody>
</table>
<a href="/records?page=2" x-target="records">Load More</a>
Live Search with AJAX
<div id="search-results" x-merge="morph">
<form action="/search" x-target="search-results">
<input name="q" type="search" @input.debounce="$el.form.requestSubmit()">
</form>
<ul>
<!-- Results here -->
</ul>
</div>
Server-Side Response Requirements
For Alpine AJAX to work correctly:
- Return only the target element - Response should contain the element with matching ID
- Preserve element ID - The returned element must have the same
idas the target - Include child content - All child elements will be used to update the target
Example Laravel controller response:
public function update(Request $request)
{
$comment = Comment::create($request->all());
// Return partial view with target element
return view('partials.comments', [
'comments' => Comment::latest()->get()
]);
}
Partial view:
<ul id="comments">
@foreach($comments as $comment)
<li>{{ $comment->text }}</li>
@endforeach
</ul>
Best Practices
- Keep components small - Each
x-datascope should manage a single concern - Use semantic HTML - Alpine enhances HTML, don't replace it
- Prefer Alpine over custom JS - Use Alpine directives before writing vanilla JavaScript
- Test without JavaScript - Ensure forms work with JS disabled (progressive enhancement)
- Use appropriate merge strategies - Choose between replace, append, prepend, and morph based on use case
- Validate on server - Always validate AJAX requests server-side, even if you validate client-side
- Handle errors gracefully - Provide user feedback when AJAX requests fail
- Follow existing patterns - Check sibling components for established conventions
Debugging
Access Alpine data in browser console:
// Get Alpine data for an element
Alpine.$data(document.querySelector('[x-data]'))
// Watch for Alpine events
window.addEventListener('alpine:init', () => console.log('Alpine initialized'))
Common Pitfalls
- Using x-if without
<template>- x-if must be on a template tag - Using x-for without
<template>- x-for must be on a template tag - Forgetting x-target - Forms/links need x-target for AJAX behavior
- Mismatched IDs - Server response element ID must match x-target value
- Not preserving state - Use x-merge="morph" when you need to preserve form state during updates