Alpine.js
Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going.
Alpine is a collection of 15 attributes, 6 properties, and 2 methods.
Install
Include the JS in your <head>:
<head>
...
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>Directives
x-data
Everything in Alpine starts with the x-data directive.
It defines a chunk of HTML as an Alpine component and provides the reactive data for that component to reference.
Scope
Properties defined in an x-data directive are available to all element children. Even ones inside other, nested x-data components.
For example:
<div x-data="{ foo: 'bar' }">
<span x-text="foo"><!-- Will output: "bar" --></span>
<div x-data="{ bar: 'baz' }">
<span x-text="foo"><!-- Will output: "bar" --></span>
<div x-data="{ foo: 'bob' }">
<span x-text="foo"><!-- Will output: "bob" --></span>
</div>
</div>
</div>Methods
Because x-data is evaluated as a normal JavaScript object, in addition to state, you can store methods and even getters.
For example, let’s extract the “Toggle Content” behavior into a method on x-data.
<div x-data="{ open: false, toggle() { this.open = ! this.open } }">
<button @click="toggle()">Toggle Content</button>
<div x-show="open">
Content...
</div>
</div>Notice the added toggle() { this.open = ! this.open } method on x-data. This method can now be called from anywhere inside the component.
You’ll also notice the usage of this. to access state on the object itself. This is because Alpine evaluates this data object like any standard JavaScript object with a this context.
If you prefer, you can leave the calling parenthesis off of the toggle method completely. For example:
<!-- Before -->
<button @click="toggle()">...</button>
<!-- After -->
<button @click="toggle">...</button>Getters
JavaScript getters are handy when the sole purpose of a method is to return data based on other state.
Think of them like “computed properties” (although, they are not cached like Vue’s computed properties).
Let’s refactor our component to use a getter called isOpen instead of accessing open directly.
<div x-data="{
open: false,
get isOpen() { return this.open },
toggle() { this.open = ! this.open },
}">
<button @click="toggle()">Toggle Content</button>
<div x-show="isOpen">
Content...
</div>
</div>Notice the “Content” now depends on the isOpen getter instead of the open property directly.
In this case there is no tangible benefit. But in some cases, getters are helpful for providing a more expressive syntax in your components.
Data-less components
Occasionally, you want to create an Alpine component, but you don’t need any data.
In these cases, you can always pass in an empty object.
<div x-data="{}">However, if you wish, you can also eliminate the attribute value entirely if it looks better to you.
<div x-data>Re-usable Data
If you find yourself duplicating the contents of x-data, or you find the inline syntax verbose, you can extract the x-data object out to a dedicated component using Alpine.data.
Here’s a quick example:
<div x-data="dropdown">
<button @click="toggle">Toggle Content</button>
<div x-show="open">
Content...
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = ! this.open
},
}))
})
</script>x-init
The x-init directive allows you to hook into the initialization phase of any element in Alpine.
<div x-init="console.log('I\'m being initialized!')"></div>In the above example, “I’m being initialized!” will be output in the console before it makes further DOM updates.
Consider another example where x-init is used to fetch some JSON and store it in x-data before the component is processed.
<div
x-data="{ posts: [] }"
x-init="posts = await (await fetch('/posts')).json()"
>...</div>$nextTick
Sometimes, you want to wait until after Alpine has completely finished rendering to execute some code.
This would be something like useEffect(..., []) in react, or mount in Vue.
By using Alpine’s internal $nextTick magic, you can make this happen.
<div x-init="$nextTick(() => { ... })"></div>Auto-evaluate init() method
If the x-data object of a component contains an init() method, it will be called automatically. For example:
<div x-data="{
init() {
console.log('I am called automatically')
}
}">
...
</div>This is also the case for components that were registered using the Alpine.data() syntax.
Alpine.data('dropdown', () => ({
init() {
console.log('I will get evaluated when initializing each "dropdown" component.')
},
}))If you have both an x-data object containing an init() method and an x-init directive, the x-data method will be called before the directive.
<div
x-data="{
init() {
console.log('I am called first')
}
}"
x-init="console.log('I am called second')"
>
...
</div>x-show
x-show is one of the most useful and powerful directives in Alpine. It provides an expressive way to show and hide DOM elements.
Here’s an example of a simple dropdown component using x-show.
<div x-data="{ open: false }">
<button x-on:click="open = ! open">Toggle Dropdown</button>
<div x-show="open">
Dropdown Contents...
</div>
</div>When the “Toggle Dropdown” button is clicked, the dropdown will show and hide accordingly due to the var open.
x-bind
x-bind allows you to set HTML attributes on elements based on the result of JavaScript expressions.
For example, here’s a component where we will use x-bind to set the placeholder value of an input.
<div x-data="{ placeholder: 'Type here...' }">
<input type="text" x-bind:placeholder="placeholder">
</div>If x-bind: is too verbose for your liking, you can use the shorthand: :. For example, here is the same input element as above, but refactored to use the shorthand syntax.
<input type="text" :placeholder="placeholder">Note: Despite not being included in the above snippet,
x-bindcannot be used if no parent element hasx-datadefined.
Binding classes
x-bind is most often useful for setting specific classes on an element based on your Alpine state.
Here’s a simple example of a simple dropdown toggle, but instead of using x-show, we’ll use a “hidden” class to toggle an element.
<div x-data="{ open: false }">
<button x-on:click="open = ! open">Toggle Dropdown</button>
<div :class="open ? '' : 'hidden'">
Dropdown Contents...
</div>
</div>Now, when open is false, the “hidden” class will be added to the dropdown.
Shorthand conditionals
In cases like these, if you prefer a less verbose syntax you can use JavaScript’s short-circuit evaluation instead of standard conditionals:
<div :class="show ? '' : 'hidden'">
<!-- Is equivalent to: -->
<div :class="show || 'hidden'">The inverse is also available to you. Suppose instead of open, we use a variable with the opposite value: closed.
<div :class="closed ? 'hidden' : ''">
<!-- Is equivalent to: -->
<div :class="closed && 'hidden'">Class object syntax
Alpine offers an additional syntax for toggling classes if you prefer. By passing a JavaScript object where the classes are the keys and booleans are the values, Alpine will know which classes to apply and which to remove. For example:
<div :class="{ 'hidden': ! show }">This technique offers a unique advantage to other methods. When using object-syntax, Alpine will NOT preserve original classes applied to an element’s class attribute.
For example, if you wanted to apply the “hidden” class to an element before Alpine loads, AND use Alpine to toggle its existence you can only achieve that behavior using object-syntax:
<div class="hidden" :class="{ 'hidden': ! show }">In case that confused you, let’s dig deeper into how Alpine handles x-bind:class differently than other attributes.
Special behavior
x-bind:class behaves differently than other attributes under the hood.
Consider the following case.
<div class="opacity-50" :class="hide && 'hidden'">If “class” were any other attribute, the :class binding would overwrite any existing class attribute, causing “opacity-50” to be overwritten by either hidden or ”.
However, Alpine treats class bindings differently. It’s smart enough to preserve existing classes on an element.
For example, if hide is true, the above example will result in the following DOM element:
<div class="opacity-50 hidden">If hide is false, the DOM element will look like:
<div class="opacity-50">This behavior should be invisible and intuitive to most users, but it is worth mentioning explicitly for the inquiring developer or any special cases that might crop up.
Binding styles
Similar to the special syntax for binding classes with JavaScript objects, Alpine also offers an object-based syntax for binding style attributes.
Just like the class objects, this syntax is entirely optional. Only use it if it affords you some advantage.
<div :style="{ color: 'red', display: 'flex' }">
<!-- Will render: -->
<div style="color: red; display: flex;" ...>Conditional inline styling is possible using expressions just like with x-bind:class. Short circuit operators can be used here as well by using a styles object as the second operand.
<div x-bind:style="true && { color: 'red' }">
<!-- Will render: -->
<div style="color: red;">One advantage of this approach is being able to mix it in with existing styles on an element:
<div style="padding: 1rem;" :style="{ color: 'red', display: 'flex' }">
<!-- Will render: -->
<div style="padding: 1rem; color: red; display: flex;" ...>And like most expressions in Alpine, you can always use the result of a JavaScript expression as the reference:
<div x-data="{ styles: { color: 'red', display: 'flex' }}">
<div :style="styles">
</div>
<!-- Will render: -->
<div ...>
<div style="color: red; display: flex;" ...>
</div>x-on
x-on allows you to easily run code on dispatched DOM events.
Here’s an example of simple button that shows an alert when clicked.
<button x-on:click="alert('Hello World!')">Say Hi</button>If x-on: is too verbose for your tastes, you can use the shorthand syntax: @.
Here’s the same component as above, but using the shorthand syntax instead:
<button @click="alert('Hello World!')">Say Hi</button>Note: Despite not being included in the above snippet,
x-oncannot be used if no parent element hasx-datadefined.
event object
If you wish to access the native JavaScript event object from your expression, you can use Alpine’s magic $event property.
<button @click="alert($event.target.getAttribute('message'))" message="Hello World">Say Hi</button>In addition, Alpine also passes the event object to any methods referenced without trailing parenthesis. For example:
<button @click="handleClick">...</button>
<script>
function handleClick(e) {
// Now you can access the event object (e) directly
}
</script>Keyboard events
Alpine makes it easy to listen for keydown and keyup events on specific keys.
Here’s an example of listening for the Enter key inside an input element.
<input type="text" @keyup.enter="alert('Submitted!')">You can also chain these key modifiers to achieve more complex listeners.
Here’s a listener that runs when the Shift key is held and Enter is pressed, but not when Enter is pressed alone.
<input type="text" @keyup.shift.enter="alert('Submitted!')">You can directly use any valid key names exposed via KeyboardEvent.key as modifiers by converting them to kebab-case.
<input type="text" @keyup.page-down="alert('Submitted!')">For easy reference, here is a list of common keys you may want to listen for.
| Modifier | Keyboard Key |
|---|---|
.shift | Shift |
.enter | Enter |
.space | Space |
.ctrl | Ctrl |
.cmd | Cmd |
.meta | Cmd on Mac, Windows key on Windows |
.alt | Alt |
.up .down .left .right | Up/Down/Left/Right arrows |
.escape | Escape |
.tab | Tab |
.caps-lock | Caps Lock |
.equal | Equal, = |
.period | Period, . |
.comma | Comma, , |
.slash | Forward Slash, / |
Mouse events
Like the above Keyboard Events, Alpine allows the use of some key modifiers for handling click events.
| Modifier | Event Key |
|---|---|
.shift | shiftKey |
.ctrl | ctrlKey |
.cmd | metaKey |
.meta | metaKey |
.alt | altKey |
These work on click, auxclick, context and dblclick events, and even mouseover, mousemove, mouseenter, mouseleave, mouseout, mouseup and mousedown.
Here’s an example of a button that changes behaviour when the Shift key is held down.
<button type="button"
@click="message = 'selected'"
@click.shift="message = 'added to selection'">
@mousemove.shift="message = 'add to selection'"
@mouseout="message = 'select'"
x-text="message"></button>
Note: Normal click events with some modifiers (like ctrl) will automatically become contextmenu events in most browsers. Similarly, right-click events will trigger a contextmenu event, but will also trigger an auxclick event if the contextmenu event is prevented.Custom events
Alpine event listeners are a wrapper for native DOM event listeners. Therefore, they can listen for ANY DOM event, including custom events.
Here’s an example of a component that dispatches a custom DOM event and listens for it as well.
<div x-data @foo="alert('Button Was Clicked!')">
<button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
</div>When the button is clicked, the @foo listener will be called.
Because the .dispatchEvent API is verbose, Alpine offers a $dispatch helper to simplify things.
Here’s the same component re-written with the $dispatch magic property.
<div x-data @foo="alert('Button Was Clicked!')">
<button @click="$dispatch('foo')">...</button>
</div>Modifiers
Alpine offers a number of directive modifiers to customize the behavior of your event listeners.
.prevent
.prevent is the equivalent of calling .preventDefault() inside a listener on the browser event object.
<form @submit.prevent="console.log('submitted')" action="/foo">
<button>Submit</button>
</form>In the above example, with the .prevent, clicking the button will NOT submit the form to the /foo endpoint. Instead, Alpine’s listener will handle it and “prevent” the event from being handled any further.
.stop
Similar to .prevent, .stop is the equivalent of calling .stopPropagation() inside a listener on the browser event object.
<div @click="console.log('I will not get logged')">
<button @click.stop>Click Me</button>
</div>In the above example, clicking the button WON’T log the message. This is because we are stopping the propagation of the event immediately and not allowing it to “bubble” up to the <div> with the @click listener on it.
.outside
.outside is a convenience helper for listening for a click outside of the element it is attached to. Here’s a simple dropdown component example to demonstrate:
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle</button>
<div x-show="open" @click.outside="open = false">
Contents...
</div>
</div>In the above example, after showing the dropdown contents by clicking the “Toggle” button, you can close the dropdown by clicking anywhere on the page outside the content.
This is because .outside is listening for clicks that DON’T originate from the element it’s registered on.
It’s worth noting that the
.outsideexpression will only be evaluated when the element it’s registered on is visible on the page. Otherwise, there would be nasty race conditions where clicking the “Toggle” button would also fire the@click.outsidehandler when it is not visible.
.window
When the .window modifier is present, Alpine will register the event listener on the root window object on the page instead of the element itself.
<div @keyup.escape.window="...">...</div>The above snippet will listen for the “escape” key to be pressed ANYWHERE on the page.
Adding .window to listeners is extremely useful for these sorts of cases where a small part of your markup is concerned with events that take place on the entire page.
.document
.document works similarly to .window only it registers listeners on the document global, instead of the window global.
.once
By adding .once to a listener, you are ensuring that the handler is only called ONCE.
<button @click.once="console.log('I will only log once')">...</button>.debounce
Sometimes it is useful to “debounce” an event handler so that it only is called after a certain period of inactivity (250 milliseconds by default).
For example if you have a search field that fires network requests as the user types into it, adding a debounce will prevent the network requests from firing on every single keystroke.
<input @input.debounce="fetchResults">Now, instead of calling fetchResults after every keystroke, fetchResults will only be called after 250 milliseconds of no keystrokes.
If you wish to lengthen or shorten the debounce time, you can do so by trailing a duration after the .debounce modifier like so:
<input @input.debounce.500ms="fetchResults">Now, fetchResults will only be called after 500 milliseconds of inactivity.
.throttle
.throttle is similar to .debounce except it will release a handler call every 250 milliseconds instead of deferring it indefinitely.
This is useful for cases where there may be repeated and prolonged event firing and using .debounce won’t work because you want to still handle the event every so often.
For example:
<div @scroll.window.throttle="handleScroll">...</div>The above example is a great use case of throttling. Without .throttle, the handleScroll method would be fired hundreds of times as the user scrolls down a page. This can really slow down a site. By adding .throttle, we are ensuring that handleScroll only gets called every 250 milliseconds.
Just like with .debounce, you can add a custom duration to your throttled event:
<div @scroll.window.throttle.750ms="handleScroll">...</div>Now, handleScroll will only be called every 750 milliseconds.
.self
By adding .self to an event listener, you are ensuring that the event originated on the element it is declared on, and not from a child element.
<button @click.self="handleClick">
Click Me
<img src="...">
</button>In the above example, we have an <img> tag inside the <button> tag. Normally, any click originating within the <button> element (like on <img> for example), would be picked up by a @click listener on the button.
However, in this case, because we’ve added a .self, only clicking the button itself will call handleClick. Only clicks originating on the <img> element will not be handled.
.camel
<div @custom-event.camel="handleCustomEvent">
...
</div>Sometimes you may want to listen for camelCased events such as customEvent in our example. Because camelCasing inside HTML attributes is not supported, adding the .camel modifier is necessary for Alpine to camelCase the event name internally.
By adding .camel in the above example, Alpine is now listening for customEvent instead of custom-event.
.dot
Similar to the .camelCase modifier there may be situations where you want to listen for events that have dots in their name (like custom.event). Since dots within the event name are reserved by Alpine you need to write them with dashes and add the .dot modifier.
In the code example above custom-event.dot will correspond to the event name custom.event.
.passive
Browsers optimize scrolling on pages to be fast and smooth even when JavaScript is being executed on the page. However, improperly implemented touch and wheel listeners can block this optimization and cause poor site performance.
If you are listening for touch events, it’s important to add .passive to your listeners to not block scroll performance.
<div @touchstart.passive="...">...</div>.capture
Add this modifier if you want to execute this listener in the event’s capturing phase, e.g. before the event bubbles from the target element up the DOM.
<div @click.capture="console.log('I will log first')">
<button @click="console.log('I will log second')"></button>
</div>x-text
x-text sets the text content of an element to the result of a given expression.
Here’s a basic example of using x-text to display a user’s username.
<div x-data="{ username: 'calebporzio' }">
Username: <strong x-text="username"></strong>
</div>x-html
x-html sets the “innerHTML” property of an element to the result of a given expression.
⚠️ Only use on trusted content and never on user-provided content. ⚠️ Dynamically rendering HTML from third parties can easily lead to XSS vulnerabilities.
Here’s a basic example of using x-html to display a user’s username.
<div x-data="{ username: '<strong>calebporzio</strong>' }">
Username: <span x-html="username"></span>
</div>x-model
x-model allows you to bind the value of an input element to Alpine data.
Here’s a simple example of using x-model to bind the value of a text field to a piece of data in Alpine.
<div x-data="{ message: '' }">
<input type="text" x-model="message">
<span x-text="message"></span>
</div>Now as the user types into the text field, the message will be reflected in the <span> tag.
x-model is two-way bound, meaning it both “sets” and “gets”. In addition to changing data, if the data itself changes, the element will reflect the change.
We can use the same example as above but this time, we’ll add a button to change the value of the message property.
<div x-data="{ message: '' }">
<input type="text" x-model="message">
<button x-on:click="message = 'changed'">Change Message</button>
</div>Now when the <button> is clicked, the input element’s value will instantly be updated to “changed”.
x-model works with the following input elements:
<input type="text">
<textarea>
<input type="checkbox">
<input type="radio">
<select>
<input type="range">Modifiers
.lazy
On text inputs, by default, x-model updates the property on every keystroke. By adding the .lazy modifier, you can force an x-model input to only update the property when user focuses away from the input element.
This is handy for things like real-time form-validation where you might not want to show an input validation error until the user “tabs” away from a field.
<input type="text" x-model.lazy="username">
<span x-show="username.length > 20">The username is too long.</span>.number
By default, any data stored in a property via x-model is stored as a string. To force Alpine to store the value as a JavaScript number, add the .number modifier.
<input type="text" x-model.number="age">
<span x-text="typeof age"></span>.boolean
By default, any data stored in a property via x-model is stored as a string. To force Alpine to store the value as a JavaScript boolean, add the .boolean modifier. Both integers (1/0) and strings (true/false) are valid boolean values.
<select x-model.boolean="isActive">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
<span x-text="typeof isActive"></span>.debounce
By adding .debounce to x-model, you can easily debounce the updating of bound input.
This is useful for things like real-time search inputs that fetch new data from the server every time the search property changes.
<input type="text" x-model.debounce="search">The default debounce time is 250 milliseconds, you can easily customize this by adding a time modifier like so.
<input type="text" x-model.debounce.500ms="search">.throttle
Similar to .debounce you can limit the property update triggered by x-model to only updating on a specified interval.
The default throttle interval is 250 milliseconds, you can easily customize this by adding a time modifier like so.
<input type="text" x-model.throttle.500ms="search">.fill
By default, if an input has a value attribute, it is ignored by Alpine and instead, the value of the input is set to the value of the property bound using x-model.
But if a bound property is empty, then you can use an input’s value attribute to populate the property by adding the .fill modifier.
“x-modelable`
x-modelable allows you to expose any Alpine property as the target of the x-model directive.
Here’s a simple example of using x-modelable to expose a variable for binding with x-model.
<div x-data="{ number: 5 }">
<div x-data="{ count: 0 }" x-modelable="count" x-model="number">
<button @click="count++">Increment</button>
</div>
Number: <span x-text="number"></span>
</div>As you can see the outer scope property “number” is now bound to the inner scope property “count”.
Typically this feature would be used in conjunction with a backend templating framework like Laravel Blade. It’s useful for abstracting away Alpine components into backend templates and exposing state to the outside through x-model as if it were a native input.
x-for
Alpine’s x-for directive allows you to create DOM elements by iterating through a list. Here’s a simple example of using it to create a list of colors based on an array.
<ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
<template x-for="color in colors">
<li x-text="color"></li>
</template>
</ul>You may also pass objects to x-for.
<ul x-data="{ car: { make: 'Jeep', model: 'Grand Cherokee', color: 'Black' } }">
<template x-for="(value, index) in car">
<li>
<span x-text="index"></span>: <span x-text="value"></span>
</li>
</template>
</ul>There are two rules worth noting about x-for:
x-forMUST be declared on a<template>element. That<template>element MUST contain only one root element
Keys
It is important to specify unique keys for each x-for iteration if you are going to be re-ordering items. Without dynamic keys, Alpine may have a hard time keeping track of what re-orders and will cause odd side-effects.
<ul x-data="{ colors: [
{ id: 1, label: 'Red' },
{ id: 2, label: 'Orange' },
{ id: 3, label: 'Yellow' },
]}">
<template x-for="color in colors" :key="color.id">
<li x-text="color.label"></li>
</template>
</ul>Now if the colors are added, removed, re-ordered, or their “id”s change, Alpine will preserve or destroy the iterated <li> elements accordingly.
Accessing indexes
If you need to access the index of each item in the iteration, you can do so using the ([item], [index]) in [items] syntax like so:
<ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
<template x-for="(color, index) in colors">
<li>
<span x-text="index + ': '"></span>
<span x-text="color"></span>
</li>
</template>
</ul>You can also access the index inside a dynamic :key expression.
<template x-for="(color, index) in colors" :key="index">Iterating over a range
If you need to simply loop n number of times, rather than iterate through an array, Alpine offers a short syntax.
<ul>
<template x-for="i in 10">
<li x-text="i"></li>
</template>
</ul>i in this case can be named anything you like.
Despite not being included in the above snippet,
x-forcannot be used if no parent element hasx-datadefined.
Contents of a <template>
As mentioned above, an <template> tag must contain only one root element.
For example, the following code will not work:
<template x-for="color in colors">
<span>The next color is </span><span x-text="color">
</template>but this code will work:
<template x-for="color in colors">
<p>
<span>The next color is </span><span x-text="color">
</p>
</template>“x-transition`
Alpine provides a robust transitions utility out of the box. With a few x-transition directives, you can create smooth transitions between when an element is shown or hidden.
There are two primary ways to handle transitions in Alpine:
- The Transition Helper
- Applying CSS Classes
The transition helper
The simplest way to achieve a transition using Alpine is by adding x-transition to an element with x-show on it. For example:
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle</button>
<div x-show="open" x-transition>
Hello 👋
</div>
</div>As you can see, by default, x-transition applies pleasant transition defaults to fade and scale the revealing element.
You can override these defaults with modifiers attached to x-transition. Let’s take a look at those.
Customizing duration
Initially, the duration is set to be 150 milliseconds when entering, and 75 milliseconds when leaving.
You can configure the duration you want for a transition with the .duration modifier:
<div ... x-transition.duration.500ms>The above <div> will transition for 500 milliseconds when entering, and 500 milliseconds when leaving.
If you wish to customize the durations specifically for entering and leaving, you can do that like so:
<div ...
x-transition:enter.duration.500ms
x-transition:leave.duration.400ms
>Despite not being included in the above snippet,
x-transitioncannot be used if no parent element hasx-datadefined.
Customizing delay
You can delay a transition using the .delay modifier like so:
<div ... x-transition.delay.50ms>The above example will delay the transition and in and out of the element by 50 milliseconds.
Customizing opacity
By default, Alpine’s x-transition applies both a scale and opacity transition to achieve a “fade” effect.
If you wish to only apply the opacity transition (no scale), you can accomplish that like so:
<div ... x-transition.opacity>Customizing scale
Similar to the .opacity modifier, you can configure x-transition to ONLY scale (and not transition opacity as well) like so:
<div ... x-transition.scale>The .scale modifier also offers the ability to configure its scale values AND its origin values:
<div ... x-transition.scale.80>The above snippet will scale the element up and down by 80%.
Again, you may customize these values separately for enter and leaving transitions like so:
<div ...
x-transition:enter.scale.80
x-transition:leave.scale.90
>To customize the origin of the scale transition, you can use the .origin modifier:
<div ... x-transition.scale.origin.top>Now the scale will be applied using the top of the element as the origin, instead of the center by default.
Like you may have guessed, the possible values for this customization are: top, bottom, left, and right.
If you wish, you can also combine two origin values. For example, if you want the origin of the scale to be “top right”, you can use: .origin.top.right as the modifier.
Applying CSS classes
For direct control over exactly what goes into your transitions, you can apply CSS classes at different stages of the transition.
The following examples use TailwindCSS utility classes.
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle</button>
<div
x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90"
>Hello 👋</div>
</div>| Directive | Description |
|---|---|
:enter | Applied during the entire entering phase. |
:enter-start | Added before element is inserted, removed one frame after element is inserted. |
:enter-end | Added one frame after element is inserted (at the same time enter-start is removed), removed when transition/animation finishes. |
:leave | Applied during the entire leaving phase. |
:leave-start | Added immediately when a leaving transition is triggered, removed after one frame. |
:leave-end | Added one frame after a leaving transition is triggered (at the same time leave-start is removed), removed when the transition/animation finishes. |
x-effect
x-effect is a useful directive for re-evaluating an expression when one of its dependencies change. You can think of it as a watcher where you don’t have to specify what property to watch, it will watch all properties used within it.
If this definition is confusing for you, that’s ok. It’s better explained through an example:
<div x-data="{ label: 'Hello' }" x-effect="console.log(label)">
<button @click="label += ' World!'">Change Message</button>
</div>When this component is loaded, the x-effect expression will be run and “Hello” will be logged into the console.
Because Alpine knows about any property references contained within x-effect, when the button is clicked and label is changed, the effect will be re-triggered and “Hello World!” will be logged to the console.
x-ignore
By default, Alpine will crawl and initialize the entire DOM tree of an element containing x-init or x-data.
If for some reason, you don’t want Alpine to touch a specific section of your HTML, you can prevent it from doing so using x-ignore.
<div x-data="{ label: 'From Alpine' }">
<div x-ignore>
<span x-text="label"></span>
</div>
</div>In the above example, the <span> tag will not contain “From Alpine” because we told Alpine to ignore the contents of the div completely.
x-ref
x-ref in combination with $refs is a useful utility for easily accessing DOM elements directly. It’s most useful as a replacement for APIs like getElementById and querySelector.
<button @click="$refs.text.remove()">Remove Text</button>
<span x-ref="text">Hello 👋</span>x-cloak
Sometimes, when you’re using AlpineJS for a part of your template, there is a “blip” where you might see your uninitialized template after the page loads, but before Alpine loads.
x-cloak addresses this scenario by hiding the element it’s attached to until Alpine is fully loaded on the page.
For x-cloak to work however, you must add the following CSS to the page.
[x-cloak] { display: none !important; }The following example will hide the <span> tag until its x-show is specifically set to true, preventing any “blip” of the hidden element onto screen as Alpine loads.
<span x-cloak x-show="false">This will not 'blip' onto screen at any point</span>x-cloak doesn’t just work on elements hidden by x-show or x-if: it also ensures that elements containing data are hidden until the data is correctly set. The following example will hide the <span> tag until Alpine has set its text content to the message property.
<span x-cloak x-text="message"></span>When Alpine loads on the page, it removes all x-cloak property from the element, which also removes the display: none; applied by CSS, therefore showing the element.
Alternative to global syntax
If you’d like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittedly odd trick:
<template x-if="true">
<span x-text="message"></span>
</template>This will achieve the same goal as x-cloak by just leveraging the way x-if works.
Because <template> elements are “hidden” in browsers by default, you won’t see the <span> until Alpine has had a chance to render the x-if="true" and show it.
Again, this solution is not for everyone, but it’s worth mentioning for special cases.
x-teleport
The x-teleport directive allows you to transport part of your Alpine template to another part of the DOM on the page entirely.
This is useful for things like modals (especially nesting them), where it’s helpful to break out of the z-index of the current Alpine component.
By attaching x-teleport to a <template> element, you are telling Alpine to “append” that element to the provided selector.
The x-teleport selector can be any string you would normally pass into something like document.querySelector. It will find the first element that matches, be it a tag name (body), class name (.my-class), ID (#my-id), or any other valid CSS selector.
Here’s a contrived modal example:
<body>
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle Modal</button>
<template x-teleport="body">
<div x-show="open">
Modal contents...
</div>
</template>
</div>
<div>Some other content placed AFTER the modal markup.</div>
...
</body>Forwarding events
Alpine tries its best to make the experience of teleporting seamless. Anything you would normally do in a template, you should be able to do inside an x-teleport template. Teleported content can access the normal Alpine scope of the component as well as other features like $refs, $root, etc…
However, native DOM events have no concept of teleportation, so if, for example, you trigger a “click” event from inside a teleported element, that event will bubble up the DOM tree as it normally would.
To make this experience more seamless, you can “forward” events by simply registering event listeners on the <template x-teleport...> element itself like so:
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle Modal</button>
<template x-teleport="body" @click="open = false">
<div x-show="open">
Modal contents...
(click to close)
</div>
</template>
</div>Notice how we are now able to listen for events dispatched from within the teleported element from outside the <template> element itself?
Alpine does this by looking for event listeners registered on <template x-teleport...> and stops those events from propagating past the live, teleported, DOM element. Then, it creates a copy of that event and re-dispatches it from <template x-teleport...>.
Nesting
Teleporting is especially helpful if you are trying to nest one modal within another. Alpine makes it simple to do so:
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle Modal</button>
<template x-teleport="body">
<div x-show="open">
Modal contents...
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle Nested Modal</button>
<template x-teleport="body">
<div x-show="open">
Nested modal contents...
</div>
</template>
</div>
</div>
</template>
</div>After toggling “on” both modals, they are authored as children, but will be rendered as sibling elements on the page, not within one another.
x-if
x-if is used for toggling elements on the page, similarly to x-show, however it completely adds and removes the element it’s applied to rather than just changing its CSS display property to “none”.
Because of this difference in behavior, x-if should not be applied directly to the element, but instead to a <template> tag that encloses the element. This way, Alpine can keep a record of the element once it’s removed from the page.
<template x-if="open">
<div>Contents...</div>
</template>x-id
x-id allows you to declare a new “scope” for any new IDs generated using $id(). It accepts an array of strings (ID names) and adds a suffix to each $id('...') generated within it that is unique to other IDs on the page.
x-id is meant to be used in conjunction with the $id(...) magic.
Visit the $id documentation for a better understanding of this feature.
Here’s a brief example of this directive in use:
<div x-id="['text-input']">
<label :for="$id('text-input')">Username</label>
<!-- for="text-input-1" -->
<input type="text" :id="$id('text-input')">
<!-- id="text-input-1" -->
</div>
<div x-id="['text-input']">
<label :for="$id('text-input')">Username</label>
<!-- for="text-input-2" -->
<input type="text" :id="$id('text-input')">
<!-- id="text-input-2" -->
</div>Magics
$el
$el is a magic property that can be used to retrieve the current DOM node.
<button @click="$el.innerHTML = 'Hello World!'">Replace me with "Hello World!"</button>$refs
$refs is a magic property that can be used to retrieve DOM elements marked with x-ref inside the component. This is useful when you need to manually manipulate DOM elements. It’s often used as a more succinct, scoped, alternative to document.querySelector.
<button @click="$refs.text.remove()">Remove Text</button>
<span x-ref="text">Hello 👋</span>$store
You can use $store to conveniently access global Alpine stores registered using Alpine.store(...). For example:
<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>
...
<div x-data :class="$store.darkMode.on && 'bg-black'">
...
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', {
on: false,
toggle() {
this.on = ! this.on
}
})
})
</script>Given that we’ve registered the darkMode store and set on to “false”, when the <button> is pressed, on will be “true” and the background color of the page will change to black.
Single-value stores
If you don’t need an entire object for a store, you can set and use any kind of data as a store.
Here’s the example from above but using it more simply as a boolean value:
<button x-data @click="$store.darkMode = ! $store.darkMode">Toggle Dark Mode</button>
...
<div x-data :class="$store.darkMode && 'bg-black'">
...
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', false)
})
</script>$watch
You can “watch” a component property using the $watch magic method. For example:
<div x-data="{ open: false }" x-init="$watch('open', value => console.log(value))">
<button @click="open = ! open">Toggle Open</button>
</div>In the above example, when the button is pressed and open is changed, the provided callback will fire and console.log the new value:
You can watch deeply nested properties using “dot” notation
<div x-data="{ foo: { bar: 'baz' }}" x-init="$watch('foo.bar', value => console.log(value))">
<button @click="foo.bar = 'bob'">Toggle Open</button>
</div>When the <button> is pressed, foo.bar will be set to “bob”, and “bob” will be logged to the console.
Getting the “old” value
$watch keeps track of the previous value of the property being watched, You can access it using the optional second argument to the callback like so:
<div x-data="{ open: false }" x-init="$watch('open', (value, oldValue) => console.log(value, oldValue))">
<button @click="open = ! open">Toggle Open</button>
</div>Deep watching
$watch automatically watches from changes at any level but you should keep in mind that, when a change is detected, the watcher will return the value of the observed property, not the value of the subproperty that has changed.
<div x-data="{ foo: { bar: 'baz' }}" x-init="$watch('foo', (value, oldValue) => console.log(value, oldValue))">
<button @click="foo.bar = 'bob'">Update</button>
</div>When the <button> is pressed, foo.bar will be set to “bob”, and “{bar: ‘bob’} {bar: ‘baz’}” will be logged to the console (new and old value).
⚠️ Changing a property of a “watched” object as a side effect of the
$watchcallback will generate an infinite loop and eventually error.
<!-- 🚫 Infinite loop -->
<div x-data="{ foo: { bar: 'baz', bob: 'lob' }}" x-init="$watch('foo', value => foo.bob = foo.bar)">
<button @click="foo.bar = 'bob'">Update</button>
</div>$dispatch
$dispatch is a helpful shortcut for dispatching browser events.
<div @notify="alert('Hello World!')">
<button @click="$dispatch('notify')">
Notify
</button>
</div>You can also pass data along with the dispatched event if you wish. This data will be accessible as the .detail property of the event:
<div @notify="alert($event.detail.message)">
<button @click="$dispatch('notify', { message: 'Hello World!' })">
Notify
</button>
</div>Under the hood, $dispatch is a wrapper for the more verbose API: element.dispatchEvent(new CustomEvent(...))
Note on event propagation
Notice that, because of event bubbling, when you need to capture events dispatched from nodes that are under the same nesting hierarchy, you’ll need to use the .window modifier:
Example:
<!-- 🚫 Won't work -->
<div x-data>
<span @notify="..."></span>
<button @click="$dispatch('notify')">Notify</button>
</div>
<!-- ✅ Will work (because of .window) -->
<div x-data>
<span @notify.window="..."></span>
<button @click="$dispatch('notify')">Notify</button>
</div>The first example won’t work because when notify is dispatched, it’ll propagate to its common ancestor, the div, not its sibling, the <span>. The second example will work because the sibling is listening for notify at the window level, which the custom event will eventually bubble up to.
$nextTick
$nextTick is a magic property that allows you to only execute a given expression AFTER Alpine has made its reactive DOM updates. This is useful for times you want to interact with the DOM state AFTER it’s reflected any data updates you’ve made.
<div x-data="{ title: 'Hello' }">
<button
@click="
title = 'Hello World!';
$nextTick(() => { console.log($el.innerText) });
"
x-text="title"
></button>
</div>In the above example, rather than logging “Hello” to the console, “Hello World!” will be logged because $nextTick was used to wait until Alpine was finished updating the DOM.
Promises
$nextTick returns a promise, allowing the use of $nextTick to pause an async function until after pending dom updates. When used like this, $nextTick also does not require an argument to be passed.
<div x-data="{ title: 'Hello' }">
<button
@click="
title = 'Hello World!';
await $nextTick();
console.log($el.innerText);
"
x-text="title"
></button>
</div>$root
$root is a magic property that can be used to retrieve the root element of any Alpine component. In other words the closest element up the DOM tree that contains x-data.
<div x-data data-message="Hello World!">
<button @click="alert($root.dataset.message)">Say Hi</button>
</div>$data
$data is a magic property that gives you access to the current Alpine data scope (generally provided by x-data).
Most of the time, you can just access Alpine data within expressions directly. for example x-data="{ message: 'Hello Caleb!' }" will allow you to do things like x-text="message".
However, sometimes it is helpful to have an actual object that encapsulates all scope that you can pass around to other functions:
<div x-data="{ greeting: 'Hello' }">
<div x-data="{ name: 'Caleb' }">
<button @click="sayHello($data)">Say Hello</button>
</div>
</div>
<script>
function sayHello({ greeting, name }) {
alert(greeting + ' ' + name + '!')
}
</script>Now when the button is pressed, the browser will alert Hello Caleb! because it was passed a data object that contained all the Alpine scope of the expression that called it (@click="...").
Most applications won’t need this magic property, but it can be very helpful for deeper, more complicated Alpine utilities.
$id
$id is a magic property that can be used to generate an element’s ID and ensure that it won’t conflict with other IDs of the same name on the same page.
This utility is extremely helpful when building re-usable components (presumably in a back-end template) that might occur multiple times on a page, and make use of ID attributes.
Things like input components, modals, listboxes, etc. will all benefit from this utility.
Basic usage
Suppose you have two input elements on a page, and you want them to have a unique ID from each other, you can do the following:
<input type="text" :id="$id('text-input')">
<!-- id="text-input-1" -->
<input type="text" :id="$id('text-input')">
<!-- id="text-input-2" -->As you can see, $id takes in a string and spits out an appended suffix that is unique on the page.
Grouping with x-id
Now let’s say you want to have those same two input elements, but this time you want <label> elements for each of them.
This presents a problem, you now need to be able to reference the same ID twice. One for the <label>’s for attribute, and the other for the id on the input.
Here is a way that you might think to accomplish this and is totally valid:
<div x-data="{ id: $id('text-input') }">
<label :for="id"> <!-- "text-input-1" -->
<input type="text" :id="id"> <!-- "text-input-1" -->
</div>
<div x-data="{ id: $id('text-input') }">
<label :for="id"> <!-- "text-input-2" -->
<input type="text" :id="id"> <!-- "text-input-2" -->
</div>This approach is fine, however, having to name and store the ID in your component scope feels cumbersome.
To accomplish this same task in a more flexible way, you can use Alpine’s x-id directive to declare an “id scope” for a set of IDs:
<div x-id="['text-input']">
<label :for="$id('text-input')"> <!-- "text-input-1" -->
<input type="text" :id="$id('text-input')"> <!-- "text-input-1" -->
</div>
<div x-id="['text-input']">
<label :for="$id('text-input')"> <!-- "text-input-2" -->
<input type="text" :id="$id('text-input')"> <!-- "text-input-2" -->
</div>As you can see, x-id accepts an array of ID names. Now any usages of $id() within that scope, will all use the same ID. Think of them as “id groups”.
Nesting
As you might have intuited, you can freely nest these x-id groups, like so:
<div x-id="['text-input']">
<label :for="$id('text-input')"> <!-- "text-input-1" -->
<input type="text" :id="$id('text-input')"> <!-- "text-input-1" -->
<div x-id="['text-input']">
<label :for="$id('text-input')"> <!-- "text-input-2" -->
<input type="text" :id="$id('text-input')"> <!-- "text-input-2" -->
</div>
</div>Keyed IDs (For Looping)
Sometimes, it is helpful to specify an additional suffix on the end of an ID for the purpose of identifying it within a loop.
For this, $id() accepts an optional second parameter that will be added as a suffix on the end of the generated ID.
A common example of this need is something like a listbox component that uses the aria-activedescendant attribute to tell assistive technologies which element is “active” in the list:
<ul
x-id="['list-item']"
:aria-activedescendant="$id('list-item', activeItem.id)"
>
<template x-for="item in items" :key="item.id">
<li :id="$id('list-item', item.id)">...</li>
</template>
</ul>This is an incomplete example of a listbox, but it should still be helpful to demonstrate a scenario where you might need each ID in a group to still be unique to the page, but also be keyed within a loop so that you can reference individual IDs within that group.
Globals
Alpine.data()
Alpine.data(...) provides a way to re-use x-data contexts within your application.
Here’s a contrived dropdown component for example:
<div x-data="dropdown">
<button @click="toggle">...</button>
<div x-show="open">...</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = ! this.open
}
}))
})
</script>As you can see we’ve extracted the properties and methods we would usually define directly inside x-data into a separate Alpine component object.
Registering from a bundle
If you’ve chosen to use a build step for your Alpine code, you should register your components in the following way:
import Alpine from 'alpinejs'
import dropdown from './dropdown.js'
Alpine.data('dropdown', dropdown)
Alpine.start()This assumes you have a file called dropdown.js with the following contents:
export default () => ({
open: false,
toggle() {
this.open = ! this.open
}
})Initial parameters
In addition to referencing Alpine.data providers by their name plainly (like x-data="dropdown"), you can also reference them as functions (x-data="dropdown()"). By calling them as functions directly, you can pass in additional parameters to be used when creating the initial data object like so:
<div x-data="dropdown(true)">Alpine.data('dropdown', (initialOpenState = false) => ({
open: initialOpenState
}))Now, you can re-use the dropdown object, but provide it with different parameters as you need to.
Init functions
If your component contains an init() method, Alpine will automatically execute it before it renders the component. For example:
Alpine.data('dropdown', () => ({
init() {
// This code will be executed before Alpine
// initializes the rest of the component.
}
}))Destroy functions
If your component contains a destroy() method, Alpine will automatically execute it before cleaning up the component.
A primary example for this is when registering an event handler with another library or a browser API that isn’t available through Alpine. See the following example code on how to use the destroy() method to clean up such a handler.
Alpine.data('timer', () => ({
timer: null,
counter: 0,
init() {
// Register an event handler that references the component instance
this.timer = setInterval(() => {
console.log('Increased counter to', ++this.counter);
}, 1000);
},
destroy() {
// Detach the handler, avoiding memory and side-effect leakage
clearInterval(this.timer);
},
}))An example where a component is destroyed is when using one inside an x-if:
<span x-data="{ enabled: false }">
<button @click.prevent="enabled = !enabled">Toggle</button>
<template x-if="enabled">
<span x-data="timer" x-text="counter"></span>
</template>
</span>Using magic properties
If you want to access magic methods or properties from a component object, you can do so using the this context:
Alpine.data('dropdown', () => ({
open: false,
init() {
this.$watch('open', () => {...})
}
}))Alpine.store()
Alpine offers global state management through the Alpine.store() API.
Registering A Store
You can either define an Alpine store inside of an alpine:init listener (in the case of including Alpine via a <script> tag), OR you can define it before manually calling Alpine.start() (in the case of importing Alpine into a build):
From a script tag:
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', {
on: false,
toggle() {
this.on = ! this.on
}
})
})
</script>From a bundle:
import Alpine from 'alpinejs'
Alpine.store('darkMode', {
on: false,
toggle() {
this.on = ! this.on
}
})
Alpine.start()Accessing stores
You can access data from any store within Alpine expressions using the $store magic property:
<div x-data :class="$store.darkMode.on && 'bg-black'">...</div>You can also modify properties within the store and everything that depends on those properties will automatically react. For example:
<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>Additionally, you can access a store externally using Alpine.store() by omitting the second parameter like so:
<script>
Alpine.store('darkMode').toggle()
</script>Initializing stores
If you provide init() method in an Alpine store, it will be executed right after the store is registered. This is useful for initializing any state inside the store with sensible starting values.
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', {
init() {
this.on = window.matchMedia('(prefers-color-scheme: dark)').matches
},
on: false,
toggle() {
this.on = ! this.on
}
})
})
</script>Notice the newly added init() method in the example above. With this addition, the on store variable will be set to the browser’s color scheme preference before Alpine renders anything on the page.
Single-value stores
If you don’t need an entire object for a store, you can set and use any kind of data as a store.
Here’s the example from above but using it more simply as a boolean value:
<button x-data @click="$store.darkMode = ! $store.darkMode">Toggle Dark Mode</button>
...
<div x-data :class="$store.darkMode && 'bg-black'">
...
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', false)
})
</script>Alpine.bind()
Alpine.bind(...) provides a way to re-use x-bind objects within your application.
Here’s a simple example. Rather than binding attributes manually with Alpine:
<button type="button" @click="doSomething()" :disabled="shouldDisable"></button>You can bundle these attributes up into a reusable object and use x-bind to bind to that:
<button x-bind="SomeButton"></button>
<script>
document.addEventListener('alpine:init', () => {
Alpine.bind('SomeButton', () => ({
type: 'button',
'@click'() {
this.doSomething()
},
':disabled'() {
return this.shouldDisable
},
}))
})
</script>Plugins
Mask
Alpine’s Mask plugin allows you to automatically format a text input field as a user types.
This is useful for many different types of inputs: phone numbers, credit cards, dollar amounts, account numbers, dates, etc.
You can include the CDN build of this plugin as a <script> tag, just make sure to include it BEFORE Alpine’s core JS file.
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-mask
The primary API for using this plugin is the x-mask directive.
Let’s start by looking at the following simple example of a date field:
<input x-mask="99/99/9999" placeholder="MM/DD/YYYY">Notice how the text you type into the input field must adhere to the format provided by x-mask. In addition to enforcing numeric characters, the forward slashes / are also automatically added if a user doesn’t type them first.
The following wildcard characters are supported in masks:
| Wildcard | Description |
|---|---|
* | Any character |
a | Only alpha characters (a-z, A-Z) |
9 | Only numeric characters (0-9) |
Dynamic Masks
Sometimes simple mask literals (i.e. (999) 999-9999) are not sufficient. In these cases, x-mask:dynamic allows you to dynamically generate masks on the fly based on user input.
Here’s an example of a credit card input that needs to change it’s mask based on if the number starts with the numbers “34” or “37” (which means it’s an Amex card and therefore has a different format).
<input x-mask:dynamic="
$input.startsWith('34') || $input.startsWith('37')
? '9999 999999 99999' : '9999 9999 9999 9999'
">As you can see in the above example, every time a user types in the input, that value is passed to the expression as $input. Based on the $input, a different mask is utilized in the field.
x-mask:dynamic also accepts a function as a result of the expression and will automatically pass it the $input as the first parameter. For example:
<input x-mask:dynamic="creditCardMask">
<script>
function creditCardMask(input) {
return input.startsWith('34') || input.startsWith('37')
? '9999 999999 99999'
: '9999 9999 9999 9999'
}
</script>Money Inputs
Because writing your own dynamic mask expression for money inputs is fairly complex, Alpine offers a prebuilt one and makes it available as $money().
Here is a fully functioning money input mask:
<input x-mask:dynamic="$money($input)">If you wish to swap the periods for commas and vice versa (as is required in certain currencies), you can do so using the second optional parameter:
<input x-mask:dynamic="$money($input, ',')">You may also choose to override the thousands separator by supplying a third optional argument:
<input x-mask:dynamic="$money($input, '.', ' ')">You can also override the default precision of 2 digits by using any desired number of digits as the fourth optional argument:
<input x-mask:dynamic="$money($input, '.', ',', 4)">Intersect
Alpine’s Intersect plugin is a convenience wrapper for Intersection Observer that allows you to easily react when an element enters the viewport.
This is useful for: lazy loading images and other content, triggering animations, infinite scrolling, logging “views” of content, etc.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-intersect
The primary API for using this plugin is x-intersect. You can add x-intersect to any element within an Alpine component, and when that component enters the viewport (is scrolled into view), the provided expression will execute.
For example, in the following snippet, shown will remain false until the element is scrolled into view. At that point, the expression will execute and shown will become true:
<div x-data="{ shown: false }" x-intersect="shown = true">
<div x-show="shown" x-transition>
I'm in the viewport!
</div>
</div>x-intersect:enter
The :enter suffix is an alias of x-intersect, and works the same way:
<div x-intersect:enter="shown = true">...</div>You may choose to use this for clarity when also using the :leave suffix.
x-intersect:leave
Appending :leave runs your expression when the element leaves the viewport.
<div x-intersect:leave="shown = true">...</div>By default, this means the whole element is not in the viewport. Use x-intersect:leave.full to run your expression when only parts of the element are not in the viewport.
Modifiers
.once
Sometimes it’s useful to evaluate an expression only the first time an element enters the viewport and not subsequent times. For example when triggering “enter” animations. In these cases, you can add the .once modifier to x-intersect to achieve this.
<div x-intersect.once="shown = true">...</div>.half
Evaluates the expression once the intersection threshold exceeds 0.5.
Useful for elements where it’s important to show at least part of the element.
<div x-intersect.half="shown = true">...</div> // when `0.5` of the element is in the viewport.full
Evaluates the expression once the intersection threshold exceeds 0.99.
Useful for elements where it’s important to show the whole element.
<div x-intersect.full="shown = true">...</div> // when `0.99` of the element is in the viewport.threshold
Allows you to control the threshold property of the underlying IntersectionObserver:
This value should be in the range of “0-100”. A value of “0” means: trigger an “intersection” if ANY part of the element enters the viewport (the default behavior). While a value of “100” means: don’t trigger an “intersection” unless the entire element has entered the viewport.
Any value in between is a percentage of those two extremes.
For example if you want to trigger an intersection after half of the element has entered the page, you can use .threshold.50:
<div x-intersect.threshold.50="shown = true">...</div> // when 50% of the element is in the viewportIf you wanted to trigger only when 5% of the element has entered the viewport, you could use: .threshold.05, and so on and so forth.
.margin
Allows you to control the rootMargin property of the underlying IntersectionObserver. This effectively tweaks the size of the viewport boundary. Positive values expand the boundary beyond the viewport, and negative values shrink it inward. The values work like CSS margin: one value for all sides; two values for top/bottom, left/right; or four values for top, right, bottom, left. You can use px and % values, or use a bare number to get a pixel value.
<div x-intersect.margin.200px="loaded = true">...</div> // Load when the element is within 200px of the viewport
<div x-intersect:leave.margin.10%.25px.25.25px="loaded = false">...</div> // Unload when the element gets within 10% of the top of the viewport, or within 25px of the other three edges
<div x-intersect.margin.-100px="visible = true">...</div> // Mark as visible when element is more than 100 pixels into the viewport.Resize
Alpine’s Resize plugin is a convenience wrapper for the Resize Observer that allows you to easily react when an element changes size.
This is useful for: custom size-based animations, intelligent sticky positioning, conditionally adding attributes based on the element’s size, etc.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/resize@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-resize
The primary API for using this plugin is x-resize. You can add x-resize to any element within an Alpine component, and when that element is resized for any reason, the provided expression will execute with two magic properties: $width and $height.
For example, here’s a simple example of using x-resize to display the width and height of an element as it changes size.
<div
x-data="{ width: 0, height: 0 }"
x-resize="width = $width; height = $height"
>
<p x-text="'Width: ' + width + 'px'"></p>
<p x-text="'Height: ' + height + 'px'"></p>
</div>Modifiers
.document
It’s often useful to observe the entire document’s size, rather than a specific element. To do this, you can add the .document modifier to x-resize:
<div x-resize.document="...">Persist
Alpine’s Persist plugin allows you to persist Alpine state across page loads.
This is useful for persisting search filters, active tabs, and other features where users will be frustrated if their configuration is reset after refreshing or leaving and revisiting a page.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>$persist
The primary API for using this plugin is the magic $persist method.
You can wrap any value inside x-data with $persist like below to persist its value across page loads:
<div x-data="{ count: $persist(0) }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>In the above example, because we wrapped 0 in $persist(), Alpine will now intercept changes made to count and persist them across page loads.
How does it work?
If a value is wrapped in $persist, on initialization Alpine will register its own watcher for that value. Now everytime that value changes for any reason, Alpine will store the new value in localStorage.
Now when a page is reloaded, Alpine will check localStorage (using the name of the property as the key) for a value. If it finds one, it will set the property value from localStorage immediately.
You can observe this behavior by opening your browser devtool’s localStorage viewer:
Chrome devtools showing the localStorage view with count set to 0.
You’ll observe that by simply visiting this page, Alpine already set the value of “count” in localStorage. You’ll also notice it prefixes the property name “count” with “x” as a way of namespacing these values so Alpine doesn’t conflict with other tools using localStorage.
Now change the “count” in the following example and observe the changes made by Alpine to localStorage:
<div x-data="{ count: $persist(0) }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>$persist works with primitive values as well as with arrays and objects. However, it is worth noting that localStorage must be cleared when the type of the variable changes.
Given the previous example, if we change count to a value of $persist({ value: 0 }), then localStorage must be cleared or the variable ‘count’ renamed.
Setting a custom key
By default, Alpine uses the property key that $persist(...) is being assigned to (“count” in the above examples).
Consider the scenario where you have multiple Alpine components across pages or even on the same page that all use “count” as the property key.
Alpine will have no way of differentiating between these components.
In these cases, you can set your own custom key for any persisted value using the .as modifier like so:
<div x-data="{ count: $persist(0).as('other-count') }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>Now Alpine will store and retrieve the above “count” value using the key “other-count”.
By default, data is saved to localStorage, it does not have an expiration time and it’s kept even when the page is closed.
Consider the scenario where you want to clear the data once the user close the tab. In this case you can persist data to sessionStorage using the .using modifier like so:
<div x-data="{ count: $persist(0).using(sessionStorage) }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>You can also define your custom storage object exposing a getItem function and a setItem function. For example, you can decide to use a session cookie as storage doing so:
<script>
window.cookieStorage = {
getItem(key) {
let cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].split("=");
if (key == cookie[0].trim()) {
return decodeURIComponent(cookie[1]);
}
}
return null;
},
setItem(key, value) {
document.cookie = key+' = '+encodeURIComponent(value)
}
}
</script>
<div x-data="{ count: $persist(0).using(cookieStorage) }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>Using $persist with Alpine.data
If you want to use $persist with Alpine.data, you need to use a standard function instead of an arrow function so Alpine can bind a custom this context when it initially evaluates the component scope.
Alpine.data('dropdown', function () {
return {
open: this.$persist(false)
}
})Using the Alpine.$persist global
Alpine.$persist is exposed globally so it can be used outside of x-data contexts. This is useful to persist data from other sources such as Alpine.store.
Alpine.store('darkMode', {
on: Alpine.$persist(true).as('darkMode_on')
});Focus
Alpine’s Focus plugin allows you to manage focus on a page.
This plugin internally makes heavy use of the open source tool: Tabbable. Big thanks to that team for providing a much needed solution to this problem.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-trap
Focus offers a dedicated API for trapping focus within an element: the x-trap directive.
x-trap accepts a JS expression. If the result of that expression is true, then the focus will be trapped inside that element until the expression becomes false, then at that point, focus will be returned to where it was previously.
For example:
<div x-data="{ open: false }">
<button @click="open = true">Open Dialog</button>
<span x-show="open" x-trap="open">
<p>...</p>
<input type="text" placeholder="Some input...">
<input type="text" placeholder="Some other input...">
<button @click="open = false">Close Dialog</button>
</span>
</div>Nesting dialogs
Sometimes you may want to nest one dialog inside another. x-trap makes this trivial and handles it automatically.
x-trap keeps track of newly “trapped” elements and stores the last actively focused element. Once the element is “untrapped” then the focus will be returned to where it was originally.
This mechanism is recursive, so you can trap focus within an already trapped element infinite times, then “untrap” each element successively.
Here is nesting in action:
<div x-data="{ open: false }">
<button @click="open = true">Open Dialog</button>
<span x-show="open" x-trap="open">
...
<div x-data="{ open: false }">
<button @click="open = true">Open Nested Dialog</button>
<span x-show="open" x-trap="open">
...
<button @click="open = false">Close Nested Dialog</button>
</span>
</div>
<button @click="open = false">Close Dialog</button>
</span>
</div>Modifiers
.inert
When building things like dialogs/modals, it’s recommended to hide all the other elements on the page from screen readers when trapping focus.
By adding .inert to x-trap, when focus is trapped, all other elements on the page will receive aria-hidden="true" attributes, and when focus trapping is disabled, those attributes will also be removed.
<!-- When `open` is `false`: -->
<body x-data="{ open: false }">
<div x-trap.inert="open" ...>
...
</div>
<div>
...
</div>
</body>
<!-- When `open` is `true`: -->
<body x-data="{ open: true }">
<div x-trap.inert="open" ...>
...
</div>
<div aria-hidden="true">
...
</div>
</body>.noscroll
When building dialogs/modals with Alpine, it’s recommended that you disable scrolling for the surrounding content when the dialog is open.
x-trap allows you to do this automatically with the .noscroll modifiers.
By adding .noscroll, Alpine will remove the scrollbar from the page and block users from scrolling down the page while a dialog is open.
For example:
<div x-data="{ open: false }">
<button>Open Dialog</button>
<div x-show="open" x-trap.noscroll="open">
Dialog Contents
<button @click="open = false">Close Dialog</button>
</div>
</div>.noreturn
Sometimes you may not want focus to be returned to where it was previously. Consider a dropdown that’s triggered upon focusing an input, returning focus to the input on close will just trigger the dropdown to open again.
x-trap allows you to disable this behavior with the .noreturn modifier.
By adding .noreturn, Alpine will not return focus upon x-trap evaluating to false.
For example:
<div x-data="{ open: false }" x-trap.noreturn="open">
<input type="search" placeholder="search for something" />
<div x-show="open">
Search results
<button @click="open = false">Close</button>
</div>
</div>.noautofocus
By default, when x-trap traps focus within an element, it focuses the first focussable element within that element. This is a sensible default, however there are times where you may want to disable this behavior and not automatically focus any elements when x-trap engages.
By adding .noautofocus, Alpine will not automatically focus any elements when trapping focus.
$focus
This plugin offers many smaller utilities for managing focus within a page. These utilities are exposed via the $focus magic.
| Property | Description |
|---|---|
focus(el) | Focus the passed element (handling annoyances internally: using nextTick, etc.) |
focusable(el) | Detect whether or not an element is focusable |
focusables() | Get all “focusable” elements within the current element |
focused() | Get the currently focused element on the page |
lastFocused() | Get the last focused element on the page |
within(el) | Specify an element to scope the $focus magic to (the current element by default) |
first() | Focus the first focusable element |
last() | Focus the last focusable element |
next() | Focus the next focusable element |
previous() | Focus the previous focusable element |
noscroll() | Prevent scrolling to the element about to be focused |
wrap() | When retrieving “next” or “previous” use “wrap around” (e.g., returning the first element if getting the “next” element of the last element) |
getFirst() | Retrieve the first focusable element |
getLast() | Retrieve the last focusable element |
getNext() | Retrieve the next focusable element |
getPrevious() | Retrieve the previous focusable element |
Let’s walk through a few examples of these utilities in use. The example below allows the user to control focus within the group of buttons using the arrow keys. You can test this by clicking on a button, then using the arrow keys to move focus around:
<div
@keydown.right="$focus.next()"
@keydown.left="$focus.previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>Notice how if the last button is focused, pressing “right arrow” won’t do anything. Let’s add the .wrap() method so that focus “wraps around”:
<div
@keydown.right="$focus.wrap().next()"
@keydown.left="$focus.wrap().previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>Now, let’s add two buttons, one to focus the first element in the button group, and another focus the last element:
<button @click="$focus.within($refs.buttons).first()">Focus "First"</button>
<button @click="$focus.within($refs.buttons).last()">Focus "Last"</button>
<div
x-ref="buttons"
@keydown.right="$focus.wrap().next()"
@keydown.left="$focus.wrap().previous()"
>
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>Notice that we needed to add a .within() method for each button so that $focus knows to scope itself to a different element (the div wrapping the buttons).
Collapse
Alpine’s Collapse plugin allows you to expand and collapse elements using smooth animations.
Because this behavior and implementation differs from Alpine’s standard transition system, this functionality was made into a dedicated plugin.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-collapse
The primary API for using this plugin is the x-collapse directive.
x-collapse can only exist on an element that already has an x-show directive. When added to an x-show element, x-collapse will smoothly “collapse” and “expand” the element when it’s visibility is toggled by animating its height property.
For example:
<div x-data="{ expanded: false }">
<button @click="expanded = ! expanded">Toggle Content</button>
<p x-show="expanded" x-collapse>
...
</p>
</div>Modifiers
.duration
You can customize the duration of the collapse/expand transition by appending the .duration modifier like so:
<div x-data="{ expanded: false }">
<button @click="expanded = ! expanded">Toggle Content</button>
<p x-show="expanded" x-collapse.duration.1000ms>
...
</p>
</div>.min
By default, x-collapse’s “collapsed” state sets the height of the element to 0px and also sets display: none;.
Sometimes, it’s helpful to “cut-off” an element rather than fully hide it. By using the .min modifier, you can set a minimum height for x-collapse’s “collapsed” state. For example:
<div x-data="{ expanded: false }">
<button @click="expanded = ! expanded">Toggle Content</button>
<p x-show="expanded" x-collapse.min.50px>
...
</p>
</div>Anchor
Alpine’s Anchor plugin allows you to easily anchor an element’s positioning to another element on the page.
This functionality is useful when creating dropdown menus, popovers, dialogs, and tooltips with Alpine.
The “anchoring” functionality used in this plugin is provided by the Floating UI project.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>x-anchor
The primary API for using this plugin is the x-anchor directive.
To use this plugin, add the x-anchor directive to any element and pass it a reference to the element you want to anchor it’s position to (often a button on the page).
By default, x-anchor will set the element’s CSS to position: absolute and the appropriate top and left values. If the anchored element is normally displayed below the reference element but doesn’t have room on the page, it’s styling will be adjusted to render above the element.
For example, here’s a simple dropdown anchored to the button that toggles it:
<div x-data="{ open: false }">
<button x-ref="button" @click="open = ! open">Toggle</button>
<div x-show="open" x-anchor="$refs.button">
Dropdown content
</div>
</div>Positioning
x-anchor allows you to customize the positioning of the anchored element using the following modifiers:
- Bottom:
.bottom,.bottom-start,.bottom-end - Top:
.top,.top-start,.top-end - Left:
.left,.left-start,.left-end - Right:
.right,.right-start,.right-end
Here is an example of using .bottom-start to position a dropdown below and to the right of the reference element:
<div x-data="{ open: false }">
<button x-ref="button" @click="open = ! open">Toggle</button>
<div x-show="open" x-anchor.bottom-start="$refs.button">
Dropdown content
</div>
</div>Offset
You can add an offset to your anchored element using the .offset.[px value] modifier like so:
<div x-data="{ open: false }">
<button x-ref="button" @click="open = ! open">Toggle</button>
<div x-show="open" x-anchor.offset.10="$refs.button">
Dropdown content
</div>
</div>Manual styling
By default, x-anchor applies the positioning styles to your element under the hood. If you’d prefer full control over styling, you can pass the .no-style modifer and use the $anchor magic to access the values inside another Alpine expression.
Below is an example of bypassing x-anchor’s internal styling and instead applying the styles yourself using x-bind:style:
<div x-data="{ open: false }">
<button x-ref="button" @click="open = ! open">Toggle</button>
<div
x-show="open"
x-anchor.no-style="$refs.button"
x-bind:style="{ position: 'absolute', top: $anchor.y+'px', left: $anchor.x+'px' }"
>
Dropdown content
</div>
</div>Anchor to an ID
The examples thus far have all been anchoring to other elements using Alpine refs.
Because x-anchor accepts a reference to any DOM element, you can use utilities like document.getElementById() to anchor to an element by its id attribute:
<div x-data="{ open: false }">
<button id="trigger" @click="open = ! open">Toggle</button>
<div x-show="open" x-anchor="document.getElementById('trigger')">
Dropdown content
</div>
</div>Morph
Alpine’s Morph plugin allows you to “morph” an element on the page into the provided HTML template, all while preserving any browser or Alpine state within the “morphed” element.
This is useful for updating HTML from a server request without losing Alpine’s on-page state. A utility like this is at the core of full-stack frameworks like Laravel Livewire and Phoenix LiveView.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>Alpine.morph()
The Alpine.morph(el, newHtml) allows you to imperatively morph a dom node based on passed in HTML. It accepts the following parameters:
| Parameter | Description |
|---|---|
el | A DOM element on the page. |
newHtml | A string of HTML to use as the template to morph the dom element into. |
options (optional) | An options object used mainly for injecting lifecycle hooks. |
Here’s an example of using Alpine.morph() to update an Alpine component with new HTML: (In real apps, this new HTML would likely be coming from the server)
<div x-data="{ message: 'Change me, then press the button!' }">
<input type="text" x-model="message">
<span x-text="message"></span>
</div>
<button>Run Morph</button>
<script>
document.querySelector('button').addEventListener('click', () => {
let el = document.querySelector('div')
Alpine.morph(el, `
<div x-data="{ message: 'Change me, then press the button!' }">
<h2>See how new elements have been added</h2>
<input type="text" x-model="message">
<span x-text="message"></span>
<h2>but the state of this component hasn't changed? Magical.</h2>
</div>
`)
})
</script>Lifecycle Hooks
The “Morph” plugin works by comparing two DOM trees, the live element, and the passed in HTML.
Morph walks both trees simultaneously and compares each node and its children. If it finds differences, it “patches” (changes) the current DOM tree to match the passed in HTML’s tree.
While the default algorithm is very capable, there are cases where you may want to hook into its lifecycle and observe or change its behavior as it’s happening.
Before we jump into the available Lifecycle hooks themselves, let’s first list out all the potential parameters they receive and explain what each one is:
| Parameter | Description |
|---|---|
el | This is always the actual, current DOM element on the page that will be “patched” (changed by Morph). |
toEl | This is a “template element”. It’s a temporary element representing what the live el will be patched to. It will never actually live on the page and is for reference only. |
childrenOnly() | Function that can be called inside the hook to tell Morph to skip the current element and only “patch” its children. |
skip() | Function that, when called within the hook, will skip comparing/patching itself and the children of the current element. |
Here are the available lifecycle hooks (passed in as the third parameter to Alpine.morph(..., options)):
| Option | Description |
|---|---|
updating(el, toEl, childrenOnly, skip) | Called before patching the el with the comparison toEl. |
updated(el, toEl) | Called after Morph has patched el. |
removing(el, skip) | Called before Morph removes an element from the live DOM. |
removed(el) | Called after Morph has removed an element from the live DOM. |
adding(el, skip) | Called before adding a new element. |
added(el) | Called after adding a new element to the live DOM tree. |
key(el) | A reusable function to determine how Morph “keys” elements in the tree before comparing/patching. |
lookahead | A boolean value telling Morph to enable an extra feature in its algorithm that “looks ahead” to make sure a DOM element that’s about to be removed should instead just be “moved” to a later sibling. |
Here is code of all these lifecycle hooks for a more concrete reference:
Alpine.morph(el, newHtml, {
updating(el, toEl, childrenOnly, skip) {
//
},
updated(el, toEl) {
//
},
removing(el, skip) {
//
},
removed(el) {
//
},
adding(el, skip) {
//
},
added(el) {
//
},
key(el) {
// By default Alpine uses the `key=""` HTML attribute.
return el.id
},
lookahead: true, // Default: false
})Keys
Dom-diffing utilities like Morph try their best to accurately “morph” the original DOM into the new HTML. However, there are cases where it’s impossible to determine if an element should be just changed, or replaced completely.
Because of this limitation, Morph has a “key” system that allows developers to “force” preserving certain elements rather than replacing them.
The most common use-case for them is a list of siblings within a loop. Below is an example of why keys are necessary sometimes:
<!-- "Live" Dom on the page: -->
<ul>
<li>Mark</li>
<li>Tom</li>
<li>Travis</li>
</ul>
<!-- New HTML to "morph to": -->
<ul>
<li>Travis</li>
<li>Mark</li>
<li>Tom</li>
</ul>Given the above situation, Morph has no way to know that the “Travis” node has been moved in the DOM tree. It just thinks that “Mark” has been changed to “Travis” and “Travis” changed to “Tom”.
This is not what we actually want, we want Morph to preserve the original elements and instead of changing them, MOVE them within the <ul>.
By adding keys to each node, we can accomplish this like so:
<!-- "Live" Dom on the page: -->
<ul>
<li key="1">Mark</li>
<li key="2">Tom</li>
<li key="3">Travis</li>
</ul>
<!-- New HTML to "morph to": -->
<ul>
<li key="3">Travis</li>
<li key="1">Mark</li>
<li key="2">Tom</li>
</ul>Now that there are “keys” on the <li>s, Morph will match them in both trees and move them accordingly.
You can configure what Morph considers a “key” with the key: configuration option.
Sort
Alpine’s Sort plugin allows you to easily re-order elements by dragging them with your mouse.
This functionality is useful for things like Kanban boards, to-do lists, sortable table columns, etc.
The drag functionality used in this plugin is provided by the SortableJS project.
You can use this plugin by either including it from a <script> tag or installing it via NPM:
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>Basic usage
The primary API for using this plugin is the x-sort directive. By adding x-sort to an element, its children containing x-sort:item become sortable—meaning you can drag them around with your mouse, and they will change positions.
<ul x-sort>
<li x-sort:item>foo</li>
<li x-sort:item>bar</li>
<li x-sort:item>baz</li>
</ul>Sort handlers
You can react to sorting changes by passing a handler function to x-sort and adding keys to each item using x-sort:item. Here is an example of a simple handler function that shows an alert dialog with the changed item’s key and its new position:
<ul x-sort="alert($item + ' - ' + $position)">
<li x-sort:item="1">foo</li>
<li x-sort:item="2">bar</li>
<li x-sort:item="3">baz</li>
</ul>The x-sort handler will be called every time the sort order of the items change. The $item magic will contain the key of the sorted element (derived from x-sort:item), and $position will contain the new position of the item (starting at index 0).
You can also pass a handler function to x-sort and that function will receive the item and position as the first and second parameter:
<div x-data="{ handle: (item, position) => { ... } }">
<ul x-sort="handle">
<li x-sort:item="1">foo</li>
<li x-sort:item="2">bar</li>
<li x-sort:item="3">baz</li>
</ul>
</div>Handler functions are often used to persist the new order of items in the database so that the sorting order of a list is preserved between page refreshes.
Sorting groups
This plugin allows you to drag items from one x-sort sortable list into another one by adding a matching x-sort:group value to both lists:
<div>
<ul x-sort x-sort:group="todos">
<li x-sort:item="1">foo</li>
<li x-sort:item="2">bar</li>
<li x-sort:item="3">baz</li>
</ul>
<ol x-sort x-sort:group="todos">
<li x-sort:item="4">foo</li>
<li x-sort:item="5">bar</li>
<li x-sort:item="6">baz</li>
</ol>
</div>Because both sortable lists above use the same group name (todos), you can drag items from one list onto another.
When using sort handlers like x-sort="handle" and dragging an item from one group to another, only the destination list’s handler will be called with the key and new position.
Drag handles
By default, each x-sort:item element is draggable by clicking and dragging anywhere within it. However, you may want to designate a smaller, more specific element as the “drag handle” so that the rest of the element can be interacted with like normal, and only the handle will respond to mouse dragging:
<ul x-sort>
<li x-sort:item>
<span x-sort:handle> - </span>foo
</li>
<li x-sort:item>
<span x-sort:handle> - </span>bar
</li>
<li x-sort:item>
<span x-sort:handle> - </span>baz
</li>
</ul>As you can see in the above example, the hyphen ”-” is draggable, but the item text (“foo”) is not.
Ghost elements
When a user drags an item, the element will follow their mouse to appear as though they are physically dragging the element.
By default, a “hole” (empty space) will be left in the original element’s place during the drag.
If you would like to show a “ghost” of the original element in its place instead of an empty space, you can add the .ghost modifier to x-sort:
<ul x-sort.ghost>
<li x-sort:item>foo</li>
<li x-sort:item>bar</li>
<li x-sort:item>baz</li>
</ul>Styling the ghost element
By default, the “ghost” element has a .sortable-ghost CSS class attached to it while the original element is being dragged.
This makes it easy to add any custom styling you would like:
<style>
.sortable-ghost {
opacity: .5 !important;
}
</style>
<ul x-sort.ghost>
<li x-sort:item>foo</li>
<li x-sort:item>bar</li>
<li x-sort:item>baz</li>
</ul>Sorting class on body
While an element is being dragged around, Alpine will automatically add a .sorting class to the <body> element of the page.
This is useful for styling any element on the page conditionally using only CSS.
For example you could have a warning that only displays while a user is sorting items:
<div id="sort-warning">
Page functionality is limited while sorting
</div>To show this only while sorting, you can use the body.sorting CSS selector:
#sort-warning {
display: none;
}
body.sorting #sort-warning {
display: block;
}CSS hover bug
Currently, there is a bug in Chrome and Safari (not Firefox) that causes issues with hover styles.
Consider HTML like the following, where each item in the list is styled differently based on a hover state (here we’re using Tailwind’s .hover class to conditionally add a border):
<div x-sort>
<div x-sort:item class="hover:border">foo</div>
<div x-sort:item class="hover:border">bar</div>
<div x-sort:item class="hover:border">baz</div>
</div>If you drag one of the elements you will see that the hover effect will be errantly applied to any element in the original element’s place:
To fix this, you can leverage the .sorting class applied to the body while sorting to limit the hover effect to only be applied while .sorting does NOT exist on body.
Here is how you can do this directly inline using Tailwind arbitrary variants:
<div x-sort>
<div x-sort:item class="[body:not(.sorting)_&]:hover:border">foo</div>
<div x-sort:item class="[body:not(.sorting)_&]:hover:border">bar</div>
<div x-sort:item class="[body:not(.sorting)_&]:hover:border">baz</div>
</div>Now you can see that the hover effect is only applied to the dragging element and not the others in the list.
Custom configuration
Alpine chooses sensible defaults for configuring SortableJS under the hood. However, you can add or override any of these options yourself using x-sort:config:
<ul x-sort x-sort:config="{ animation: 0 }">
<li x-sort:item>foo</li>
<li x-sort:item>bar</li>
<li x-sort:item>baz</li>
</ul>Any config options passed will overwrite Alpine defaults. In this case of animation, this is fine, however be aware that overwriting handle, group, filter, onSort, onStart, or onEnd may break functionality.