Alpine.js Transitions & Plugins — Complete Guide
Alpine.js transitions and plugins extend the framework from simple reactive components into a full-featured UI toolkit — letting you animate elements smoothly, persist data across page loads, trap focus in modals, format input masks, and even create your own custom directives.
What You’ll Learn
By the end of this tutorial, you’ll control Alpine’s transition system for custom animations, use all the magic properties effectively, integrate official plugins (persist, focus, collapse, mask, intersect, morph, sort), and write your own reusable custom directives.
Why Transitions & Plugins Matter
A website without animations feels jarring. Elements appear and disappear instantly. Modals snap open without context. Users get confused about what changed and where to look.
Transitions solve this by guiding the user’s eye — a smooth fade tells the brain “something appeared here.” A slide says “new content arrived from above.” These micro-interactions make software feel polished and professional.
Plugins solve common problems without reinventing the wheel. Need to save data across page refreshes? There’s a plugin for that. Need to trap focus inside a modal for accessibility? There’s a plugin. Need to format a phone number as the user types? Plugin.
Real-world use: Durga Antivirus Pro uses Alpine’s persist plugin to remember user preferences (scan type, theme, notification settings) across sessions. The focus plugin ensures their settings dialog is keyboard-accessible. Transitions make scan results feel responsive and alive.
Where This Fits in Your Learning Path
flowchart LR
A["Getting Started"] --> B["Bindings & Events"]
B --> C["Conditionals & Loops"]
C --> D["**Transitions & Plugins**"]
D --> E["Real Alpine.js Apps"]
style D fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#fff
The x-transition System
Alpine’s transition system provides smooth CSS animations when elements enter or leave the DOM. It works with both x-show and x-if.
How Transitions Work
When you use x-transition, Alpine adds and removes CSS classes at specific moments during an element’s appearance or disappearance:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>
I fade and scale in/out
</div>
</div>With just x-transition (no customization), Alpine applies sensible defaults: 150ms ease-out for entering, 75ms ease-in for leaving, with opacity and transform transitions.
What happens step-by-step when the element appears:
- Alpine adds the element to the DOM (or makes it visible)
- It applies the start classes (e.g.,
opacity: 0; transform: scale(0.95)) - On the next frame, it removes start classes and adds end classes (e.g.,
opacity: 1; transform: scale(1)) - CSS transitions handle the animation between the two states
- After the transition duration, cleanup classes are removed
Custom Transition Directives
For full control, Alpine provides six transition directives:
<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-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90">| Directive | When it applies | What to put |
|---|---|---|
x-transition:enter | The entire enter phase | Base transition properties like duration and easing |
x-transition:enter-start | The moment enter begins | The starting state (opacity 0, scaled down) |
x-transition:enter-end | After enter-start, the final state | The ending state (opacity 1, normal size) |
x-transition:leave | The entire leave phase | Leave transition properties (often faster) |
x-transition:leave-start | The moment leave begins | Starting state of leave (same as normal) |
x-transition:leave-end | After leave-start, the final state | Ending state of leave (opacity 0, scaled down) |
Transition Modifiers
Alpine also provides shorthand modifiers for common effects:
<!-- Duration only: 500ms enter/leave -->
<div x-show="open" x-transition.duration.500ms">
<!-- Opacity only (no scale/transform) -->
<div x-show="open" x-transition.opacity">
<!-- Scale only (no opacity) -->
<div x-show="open" x-transition.scale">
<!-- Opacity and scale with custom origin -->
<div x-show="open" x-transition.scale.80.opacity.origin.top.right">
<!-- With delay -->
<div x-show="open" x-transition.delay.200ms">
<!-- Custom origin for scale transforms -->
<div x-show="open" x-transition.origin.top.right">Why choose opacity-only or scale-only? Performance. The browser can composite opacity changes on the GPU without triggering layout recalculations. If you have many elements animating simultaneously, opacity-only transitions are smoother.
Transitions with x-if
When used with x-if, Alpine intelligently waits for the leave animation to complete before removing the element from the DOM:
<template x-if="show">
<div x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
Content that animates out before DOM removal
</div>
</template>Without this behavior, x-if would snap the element out instantly. With transitions, Alpine measures the CSS transition duration from the leave classes and waits that long before removing the element.
Transition Groups (Animated Lists)
Animate items as they’re added or removed from a list:
<div x-data="{ items: [1, 2, 3] }">
<button @click="items = [...items, items.length + 1]">Add</button>
<template x-for="item in items" :key="item">
<div x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-x-4"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform translate-x-4">
<span x-text="item"></span>
</div>
</template>
</div>New items slide in from the left. Removed items slide out to the left. The rest of the list flows naturally to fill the gap.
Magic Properties Deep Dive
You’ve seen $store, $refs, and $watch already. Let’s master them and the others.
$store — Global Reactive State
The store is a shared data container accessible from any component:
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
mode: 'light',
primary: '#3b82f6',
toggle() {
this.mode = this.mode === 'light' ? 'dark' : 'light'
}
})
})
</script>
<div x-data>
<p>Current theme: <span x-text="$store.theme.mode"></span></p>
<button @click="$store.theme.toggle()">Toggle Theme</button>
</div>When to use $store: For truly global state — theme, user authentication, notification counts, shopping cart item count. If only one component needs the data, keep it in that component’s x-data.
$nextTick — After DOM Update
Execute code after Alpine has finished updating the DOM:
<div x-data="{ show: false, height: 0 }">
<button @click="
show = true;
$nextTick(() => { height = $refs.content.offsetHeight })
">
Show & Measure
</button>
<div x-show="show" x-ref="content">
<p>This is the measured content.</p>
</div>
<p x-text="`Height: ${height}px`"></p>
</div>Why $nextTick is necessary: After you set show = true, Alpine hasn’t updated the DOM yet. The content is still hidden. $nextTick waits for Alpine to finish rendering, then runs your callback. At that point, $refs.content exists and is visible, so offsetHeight returns the correct value.
$dispatch — Custom Event Communication
Alpine components communicate through custom DOM events:
<div x-data @custom-event="console.log($event.detail)">
<div x-data>
<button @click="$dispatch('custom-event', { text: 'Hello!' })">
Dispatch
</button>
</div>
</div>Events bubble up naturally. A child component dispatches, the parent (or any ancestor) catches it with @event-name.
Magic Properties Quick Reference
| Property | Type | What it does |
|---|---|---|
$store | Read/Write | Access a globally registered store |
$nextTick | Function | Run code after DOM update |
$watch | Function | Observe a property for changes |
$dispatch | Function | Fire a custom event up the DOM tree |
$el | Read-only | The root DOM element of this component |
$refs | Read-only | Elements with x-ref attributes |
$event | Read-only | Current browser event object |
$data | Read-only | Component’s reactive data object |
$root | Read-only | Root element of the closest Alpine component |
Alpine Official Plugins
Alpine’s plugin system extends its capabilities without bloating the core. Install only what you need.
@alpinejs/persist — Data That Survives Refreshes
Save state to localStorage automatically:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ count: Alpine.$persist(0) }">
<button @click="count++">Count: <span x-text="count"></span></button>
<p>Refresh the page — the count persists!</p>
</div>Alpine.$persist(0) returns a reactive proxy that syncs with localStorage. The default value is 0. Every change is automatically saved. On page load, the saved value is restored.
Custom key: Alpine.$persist(0).as('my-custom-key') — useful when you have multiple persisted values and want to avoid naming conflicts.
@alpinejs/focus — Accessible Focus Trapping
Essential for modals and dialogs. Traps keyboard focus within an element so Tab doesn’t escape behind the overlay:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ open: false }">
<button @click="open = true">Open Modal</button>
<div x-show="open"
x-trap.noscroll="open"
@click.outside="open = false"
@keydown.escape="open = false">
<h2>Modal</h2>
<input type="text" placeholder="Tab stays inside here">
<button @click="open = false">Close</button>
</div>
</div>x-trap.noscroll="open" — When open is true, Alpine traps focus inside this element. The .noscroll modifier also prevents the background page from scrolling. When open becomes false, focus trapping is released.
Why this matters for accessibility: Users who navigate by keyboard (Tab, Shift+Tab) should never be able to tab “behind” a modal overlay. Focus trapping keeps them inside the modal until they explicitly close it. This is required by WCAG accessibility guidelines.
@alpinejs/collapse — Smooth Expand/Collapse
Animates height from 0 to auto and back — something CSS alone cannot do smoothly:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ expanded: false }">
<button @click="expanded = !expanded">Toggle</button>
<div x-show="expanded" x-collapse.duration.300ms>
<p>This content will smoothly expand and collapse.</p>
<p>Height animates from 0 to auto and back.</p>
</div>
</div>The challenge x-collapse solves: CSS transition: height 0.3s doesn’t work with height: auto. You’d need a fixed pixel height, which is impractical for dynamic content. x-collapse measures the actual height and animates to/from it programmatically.
@alpinejs/mask — Input Formatting
Format values as the user types — phone numbers, prices, dates:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script>
<div x-data>
<input x-mask="(999) 999-9999" placeholder="Phone number">
<input x-mask="$99.99" placeholder="Price">
<input x-mask="99/99/9999" placeholder="Date">
</div>The 9 character represents a digit placeholder. As the user types, Alpine automatically inserts the formatting characters (parentheses, dashes, dollar sign, dots, slashes).
@alpinejs/intersect — Viewport Detection
Trigger actions when an element scrolls into view:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ visible: false }">
<div style="height: 100vh;"><!-- spacer --></div>
<div x-intersect="visible = true">
<p x-show="visible" x-transition>
I appear when you scroll down to me!
</p>
</div>
</div>Use cases:
- Lazy-loading images or content
- Triggering animations on scroll
- Tracking which sections the user has viewed (analytics)
- Implementing infinite scroll
@alpinejs/morph — Efficient DOM Transitions
Morph one DOM tree into another, preserving focus, scroll position, and input states:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ tab: 'profile' }">
<button @click="tab = 'profile'">Profile</button>
<button @click="tab = 'settings'">Settings</button>
<div x-morph:x-data="tabData">
<template x-if="tab === 'profile'">
<div><h2>Profile</h2><input type="text" placeholder="Name"></div>
</template>
<template x-if="tab === 'settings'">
<div><h2>Settings</h2><input type="text" placeholder="API Key"></div>
</template>
</div>
</div>Why morph instead of replace: If you’re typing in an input and switch tabs, a regular Alpine swap would destroy your input state. Morph preserves it. The DOM is smart-diffed — only changed elements are replaced.
@alpinejs/sort — Drag-and-Drop Lists
Make lists sortable via drag-and-drop:
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
<div x-data="{ items: ['Apple', 'Banana', 'Cherry', 'Date'] }">
<ul x-sort="items">
<template x-for="item in items" :key="item">
<li x-sort:item x-text="item"
style="padding: 0.5rem; cursor: grab; border: 1px solid #ccc; margin: 0.25rem 0;">
</li>
</template>
</ul>
</div>x-sort="items" on the container enables drag-and-drop reordering. x-sort:item on each list item makes it draggable. When the user drags and drops, the items array is automatically reordered.
Custom Directives with Alpine.directive()
When the built-in directives and plugins aren’t enough, you can create your own reusable directives.
Creating a Tooltip Directive
document.addEventListener('alpine:init', () => {
Alpine.directive('tooltip', (el, { expression }, { evaluate, cleanup }) => {
// Get the tooltip text from the expression
const text = evaluate(expression)
// Show tooltip on hover
const show = () => {
const tip = document.createElement('div')
tip.textContent = text
tip.style.cssText = 'position: absolute; background: #333; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; z-index: 1000;'
el.style.position = 'relative'
el.appendChild(tip)
el._tip = tip
}
// Hide tooltip
const hide = () => {
el._tip?.remove()
el._tip = null
}
// Attach event listeners
el.addEventListener('mouseenter', show)
el.addEventListener('mouseleave', hide)
// Cleanup when directive is removed
cleanup(() => {
el.removeEventListener('mouseenter', show)
el.removeEventListener('mouseleave', hide)
hide()
})
})
})Usage in HTML:
<button x-tooltip="'Save your changes'">Save</button>What each part does:
el— The DOM element the directive is attached to.{ expression }— The value inside the directive ('Save your changes').{ evaluate }— Safely evaluates the expression as JavaScript.cleanup— A function you call with a callback that runs when the directive is destroyed (component removed or element re-rendered). Essential for preventing memory leaks.
Custom Directive Lifecycle
| Lifecycle | When it runs |
|---|---|
init | When the directive is first bound to an element |
destroy | When the directive is torn down (component removed) |
update | When the component’s data changes and re-renders |
Common Mistakes Beginners Make
1. Forgetting to register plugins before Alpine.start()
<!-- Wrong: Alpine already started -->
<script src="alpinejs"></script>
<script>
Alpine.plugin(persist)
Alpine.start() // Already started by the CDN script!
</script>
<!-- Correct (CDN): register via alpine:init -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.plugin(persist)
})
</script>
<!-- Correct (npm): register before start -->
<script>
Alpine.plugin(persist)
Alpine.start()
</script>When using CDN, Alpine starts automatically. Register plugins inside the alpine:init event listener instead.
2. Using x-transition without defining CSS transition properties
<!-- This won't animate -->
<div x-show="open" x-transition:enter="my-enter">
<!-- Need CSS for the animation -->
<style>
.my-enter {
transition: opacity 0.3s ease;
opacity: 0;
}
</style>The CSS classes Alpine adds must have corresponding transition properties defined in your CSS. Without transition: opacity 0.3s, the opacity change is instant, not animated.
3. Not cleaning up event listeners in custom directives
<!-- Memory leak: event listeners survive component destruction -->
Alpine.directive('bad', (el) => {
window.addEventListener('resize', handler)
})
<!-- Correct: cleanup prevents memory leaks -->
Alpine.directive('good', (el, {}, { cleanup }) => {
window.addEventListener('resize', handler)
cleanup(() => window.removeEventListener('resize', handler))
})If you add global event listeners (window, document) without cleanup, they accumulate when components are destroyed and recreated. This causes memory leaks and performance degradation over time.
4. Using $store without registering it first
<!-- Wrong: store doesn't exist -->
<div x-data>
<p x-text="$store.user.name"></p>
</div>
<!-- Correct: register the store first -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('user', { name: 'Alice' })
})
</script>Alpine throws an error if you access an unregistered store.
5. Applying transitions to too many elements simultaneously
Animating 100+ items at once can cause jank (stuttering). Keep simultaneous animated elements under 50-100 items depending on the device. Use will-change: transform, opacity on animated elements for GPU acceleration.
Practice Questions
What’s the difference between
x-transition:enter-startandx-transition:enter-end?enter-startis the initial state (applied when the element first appears),enter-endis the final state. CSS transitions animate from start to end.How does
@alpinejs/persistsave data? It useslocalStorageunder the hood.Alpine.$persist(0)creates a reactive value that automatically syncs with localStorage.Why is
x-trap.noscrollimportant for accessibility? It traps keyboard focus inside a modal (so Tab doesn’t go behind it) and prevents background page scrolling — both required by WCAG accessibility standards.What does the
cleanupcallback in custom directives do? It lets you register a function that runs when the directive is destroyed, allowing you to remove event listeners and prevent memory leaks.Can you use
x-transitionwithx-if? Yes. Alpine waits for the leave animation to complete before removing the element from the DOM, unlikex-showwhere the element stays.
Challenge
Build a “notification toast system” that:
- Uses
Alpine.store()to manage a list of notifications - Each notification has a type (success, error, info) and a message
- Displays notifications using
x-forwithx-transitionfor slide-in/out animation - Auto-removes notifications after 3 seconds
- Has buttons to trigger different types of notifications
Try It Yourself
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js Animated Demo</title>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
body { font-family: system-ui; padding: 2rem; display: flex; justify-content: center; }
.card { background: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 2rem; width: 100%; max-width: 400px; }
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; }
.btn-primary { background: #3b82f6; color: white; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; }
.modal-content { background: white; border-radius: 12px; padding: 2rem; max-width: 400px; width: 90%; }
</style>
</head>
<body>
<div x-data="{ open: false }" class="card">
<h2>Animated Modal Demo</h2>
<button class="btn btn-primary" @click="open = true">Open Modal</button>
<div class="modal-overlay"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.outside="open = false"
@keydown.escape="open = false">
<div class="modal-content"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95">
<h3 style="margin-top: 0;">Animated Modal</h3>
<p>This modal fades the overlay and scales the content.</p>
<button class="btn btn-primary" @click="open = false">Close</button>
</div>
</div>
</div>
</body>
</html>What to expect: A button that opens a modal with a two-part animation — the dark overlay fades in, and the modal content scales up. Closing reverses the animation: the content scales down, then the overlay fades out.
FAQ
{< faq >}
- What is Alpinejs Transitions Plugins?
- Alpinejs Transitions Plugins refers to the core concepts and practices used to build and manage modern web applications. Understanding it is essential for web developers.
- Do I need prior experience to learn Alpinejs Transitions Plugins?
- Basic familiarity with web development concepts helps, but Alpinejs Transitions Plugins can be learned step by step even as a beginner.
- How long does it take to learn Alpinejs Transitions Plugins?
- With consistent practice, you can grasp the fundamentals in a few days to a week. Mastery takes ongoing practice and real-world projects.
- Where can I use Alpinejs Transitions Plugins in real projects?
- Alpinejs Transitions Plugins is used in a wide range of applications — from simple websites to complex enterprise systems, depending on the specific tools and technologies involved.
- What are common tools used with Alpinejs Transitions Plugins?
- The specific tools depend on the technology stack, but version control (Git), package managers, and testing frameworks are commonly used alongside most development topics.
{< /faq >}
What’s Next
You’ve mastered Alpine.js! To explore similar lightweight JavaScript tools:
| Tutorial | What You’ll Learn |
|---|---|
| Stimulus | Another lightweight JS framework |
| HTMX | AJAX-driven HTML without JavaScript |
Related topics: JavaScript fundamentals, CSS animations, Web accessibility (WCAG).
What’s Next
Congratulations on completing this Alpinejs Transitions Plugins tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro