Alpine.js v3: The Complete Directives Reference
AI generated
x-data
Alpine
Alpine.js · Directives · Frontend · Hyvä
Alpine.js v3: The Complete Directives Reference
from x-data to x-teleport, fully explained

Alpine.js v3 ships with 15 directives, 6 magic properties, and 3 global methods. Anyone who knows all of them and understands where each one belongs can build interactive UIs without a build step, without a virtual DOM, and without the complexity of large frameworks: reactive, maintainable, and written directly in HTML.

12 min read x-data · x-bind · x-on · x-model · x-show · x-if · x-for · x-transition Alpine.js 3.x · Hyvä Themes · Magento 2

1. Why Alpine.js v3 deserves a complete directives overview

Alpine.js v3 was released in May 2021 and has since established itself as the preferred framework for server rendered templates, especially alongside Laravel Blade, Hyvä Themes, and similar stacks that need no client side routing. The decisive difference from React, Vue, and Angular: Alpine.js does not touch the shadow DOM, does not build a virtual DOM diff, and does not load a hydration payload. Instead it reads directives straight from existing HTML attributes and makes an element reactive the moment it carries the x-data attribute.

The 15 directives in Alpine.js v3 cover the entire spectrum of interactive web UIs: data binding, event handling, forms, list rendering, animations, DOM manipulation, and context propagation. Knowing all of them and understanding which directive fits which situation helps avoid common mistakes, such as unnecessary DOM removal with x-if where a simple x-show would do, or performance draining watchers caused by x-effect with too broad an access scope. This article walks through all 15 directives, with concrete examples, pitfalls, and decision guidance.

For Hyvä theme developers, complete knowledge of the Alpine.js directives is especially relevant, because Hyvä relies on Alpine.js as its only JavaScript framework and fully replaces jQuery, Knockout.js, and Magento UI components. Every interactive component, from the minicart to navigation, modals, and forms, uses Alpine.js directives. Anyone who does not know these directives thoroughly ends up either writing redundant code or reaching for unnecessary JavaScript imports.

2. x-data: the origin of every reactive context

x-data is the most important directive in Alpine.js and the starting point of every reactive context. Any element carrying x-data becomes the root of an Alpine.js component. The value of x-data is a JavaScript expression that returns an object, typically an object literal or a function call. Every property on that object becomes reactive: changing it automatically triggers DOM updates in any element that accesses the same property through another directive.

As of Alpine.js v3, x-data can also be left empty (x-data with no value) when an element only needs to act as an event boundary or as a container for other directives that require no state of their own. Nested x-data elements create their own scopes that inherit from the parent scope through prototype inheritance: an inner element can read data from the outer x-data, but not the other way around. This is a fundamental difference from global stores, created with Alpine.store(), which are reachable from any context.

For more complex components, it is best to extract the logic into a named component via Alpine.data('myName', () => ({ ... })). That keeps the HTML file readable, and the JavaScript logic testable in a central file. This pattern is standard practice in Hyvä templates: components are registered in a .js file, and the template just calls x-data="myName()".


// Alpine.js v3: x-data patterns
// 1. Inline object literal
// <div x-data="{ open: false, count: 0 }">

// 2. Named component (registered globally)
Alpine.data('dropdown', () => ({
  open: false,
  toggle() { this.open = !this.open },
  close() { this.open = false },

  // init() is called automatically when component initializes
  init() {
    this.$watch('open', val => {
      document.body.classList.toggle('overflow-hidden', val)
    })
  }
}))
// <div x-data="dropdown()">

// 3. Global store, accessible from any Alpine context
Alpine.store('cart', {
  items: [],
  get count() { return this.items.length },
  add(item) { this.items.push(item) }
})
// Access: $store.cart.count (from any x-data context)

// 4. Nested scope: child inherits parent data
// <div x-data="{ color: 'teal' }">
//   <div x-data="{ size: 'lg' }">
//     <!-- Both 'color' and 'size' are accessible here -->
//   </div>
// </div>

3. x-bind and x-on: wiring attributes and events declaratively

x-bind binds the value of an HTML attribute to a JavaScript expression from the current Alpine scope. The classic example: x-bind:class="{ active: isActive }" adds the active class whenever isActive is true. The shorthand :class is identical to x-bind:class and is the preferred form in practice. x-bind works with any HTML attribute: href, src, disabled, aria-expanded, style, and even SVG attributes. Dynamically setting ARIA attributes for accessibility is a particularly useful case, covered in more detail in a dedicated article in this series.

x-on registers event listeners declaratively in HTML. The shorthand is @click instead of x-on:click. Alpine.js supports every native DOM event as well as custom events dispatched via $dispatch. Modifiers such as .prevent (calls event.preventDefault()), .stop (stopPropagation), .window (binds the event to window), .once (removes the listener after the first call), and .debounce.300ms cut down boilerplate significantly. The pattern @keydown.escape.window="close()" for closing modals with the Escape key is a concise example of how expressive these modifiers are.

4. x-model: two way binding for forms

x-model implements two way data binding for form elements: when the state changes, the input field updates; when the user types, the state updates. This works for <input>, <textarea>, <select>, checkboxes, and radio buttons. For checkboxes, x-model binds to a boolean; when the target is an array, it adds or removes the checkbox value from that array, which is ideal for multi select scenarios without a custom event handler.

The .lazy modifier delays the state update until the change event instead of firing on every keystroke, which makes sense for validation that should not run on every letter typed. .debounce.500ms throttles the update rate for live search and API calls. .number automatically converts the input value into a JavaScript number type instead of treating it as a string. .trim strips leading and trailing whitespace right at input time. In many cases these modifiers replace explicit event handlers entirely.

5. x-show vs. x-if: visibility and DOM presence

This is one of the most common decisions in Alpine.js templates, and one of the most frequently gotten wrong. x-show sets display: none or removes that style, so the element stays in the DOM and is only hidden visually. x-if inserts the element into the DOM or removes it completely. That has far reaching consequences: x-show performs better when an element is shown and hidden frequently, because no DOM parsing and no event listener setup is required. x-if makes sense when the element should not render initially (for example a modal that opens rarely) or when it contains expensive child components that should not be initialized while it is hidden.

One critical difference: x-if must sit on a <template> element, not on the element itself. This is a common mistake when moving over from Vue.js, where v-if sits directly on the element. Combining x-show with x-transition produces smooth fade in and fade out animations without any extra CSS classes. x-if, on the other hand, only triggers the transition when the element is inserted, not when it is removed, unless x-transition:leave is used explicitly.

6. x-for: rendering lists reactively

x-for renders a template element once for each entry in an array or object. Like x-if, x-for must always sit on a <template> element. The syntax x-for="item in items" is the basic form; x-for="(item, index) in items" additionally returns the index; x-for="(value, key) in object" iterates over objects. The :key attribute is technically optional but indispensable in practice: it gives Alpine.js a stable identifier it can use to reuse DOM nodes on list changes instead of recreating them, which is the difference between smooth and janky list updates.

x-for reacts to every reactive array mutation: push(), pop(), splice(), and direct index assignment. For filtered lists, a computed getter property on the x-data object is the recommended approach: get filteredItems() { return this.items.filter(i => i.active) }. Alpine.js detects that filteredItems depends on items and updates the list automatically. That is cleaner than maintaining a separate filtered array variable with an explicit watcher.


// Alpine.js v3: x-for with filtering and key binding
Alpine.data('productList', () => ({
  search: '',
  products: [
    { id: 1, name: 'Alpenjacke', active: true,  price: 129 },
    { id: 2, name: 'Wanderhose', active: true,  price: 89  },
    { id: 3, name: 'Regenschutz', active: false, price: 59  },
  ],

  // Computed getter, Alpine auto-tracks dependencies
  get filtered() {
    const q = this.search.toLowerCase()
    return this.products.filter(p =>
      p.active && p.name.toLowerCase().includes(q)
    )
  },

  toggleActive(id) {
    const p = this.products.find(p => p.id === id)
    if (p) p.active = !p.active
  }
}))

/* Template:
<div x-data="productList()">
  <input x-model.debounce.300ms="search" placeholder="Search...">
  <template x-for="product in filtered" :key="product.id">
    <div x-text="product.name + ': ' + product.price + '€'"></div>
  </template>
</div>
*/

7. x-transition: animations without CSS class chaos

x-transition adds CSS transitions to elements that are shown and hidden with x-show or x-if. In its simplest form, the bare x-transition attribute with no value is enough; Alpine.js applies a default opacity and scale transition. For custom animations there are six phase modifiers: :enter, :enter-start, :enter-end, :leave, :leave-start, and :leave-end. Each one takes Tailwind classes as its value, which Alpine.js applies and removes at exactly the right moment.

The pattern for a classic dropdown looks like this: x-transition:enter="transition ease-out duration-200", x-transition:enter-start="opacity-0 scale-95", x-transition:enter-end="opacity-100 scale-100". These six attributes replace complex animation libraries for the vast majority of UI patterns. Alternatively, x-transition can take a CSS class name as its value: x-transition="fade" then expects classes such as fade-enter-active and fade-leave-active, following the Vue naming convention, which is handy for teams migrating from Vue.js.

8. x-effect and x-ref: side effects and DOM access

x-effect is Alpine.js's answer to React's useEffect without a dependency array. The expression inside x-effect runs immediately once, and again every time a reactive property it reads changes. The tracking happens automatically, so there is no dependency list to maintain. That makes x-effect ideal for side effects such as syncing state to localStorage, setting document.title, or triggering an API call whenever a filter changes. Important: x-effect has no cleanup mechanism; for side effects that need cleanup (event listeners, timers, and similar), the logic belongs in init() with manual cleanup handling.

x-ref gives an element a name that becomes directly accessible from the Alpine scope through $refs.name. This is the Alpine.js equivalent of document.getElementById() or React's useRef. Typical use cases include focusing an input field after opening a modal (this.$refs.input.focus()), reading the scroll offset of a container element, or targeting a canvas element for drawing operations. $refs is only visible within the same Alpine component, so there is no global scope leakage.


// Alpine.js v3: x-effect, x-ref, $watch, $nextTick
Alpine.data('searchBox', () => ({
  query: '',
  results: [],
  loading: false,

  init() {
    // $watch: explicit watcher with old/new value
    this.$watch('query', async (val) => {
      if (val.length < 2) { this.results = []; return }
      this.loading = true
      this.results = await this.fetchResults(val)
      this.loading = false
      // $nextTick: run after DOM update
      this.$nextTick(() => this.$refs.resultList.scrollTop = 0)
    })
  },

  async fetchResults(q) {
    const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`)
    return res.json()
  },

  clear() {
    this.query = ''
    // Focus input after clearing
    this.$nextTick(() => this.$refs.input.focus())
  }
}))

/* x-effect example: auto sync to localStorage */
// x-effect="localStorage.setItem('theme', darkMode ? 'dark' : 'light')"
// Runs immediately and whenever darkMode changes, no watcher boilerplate needed

9. x-teleport and x-ignore: advanced DOM control

x-teleport is one of the newer directives in Alpine.js v3 and solves a classic problem: a modal or tooltip needs to appear somewhere else in the DOM than the element that triggers it. Without teleport, a modal nested deep inside a stacked position: relative container would get clipped by z-index issues and overflow: hidden. With x-teleport="#modal-root" on a <template> element, its content is moved into the specified target element, but the Alpine.js scope stays connected. That means the teleported content can still access the source component's data and send events in both directions.

x-ignore is the counterpart: it tells Alpine.js to fully ignore an element and all of its children, so no directives are processed and no reactive context is created. This is useful for elements containing server rendered code blocks, for third party widgets that manage their own DOM tree (an embedded map or a rich text editor, for example), or for raw template HTML that only gets initialized through JavaScript later. Without x-ignore, Alpine.js would try to interpret Alpine directives in that area, which can lead to errors or unwanted behavior.

10. Summary and decision guide

The 15 Alpine.js v3 directives fall into four groups: State & Scope (x-data), Binding (x-bind, x-model, x-text, x-html), Rendering (x-show, x-if, x-for, x-transition, x-cloak) and Utilities (x-on, x-effect, x-ref, x-id, x-teleport, x-ignore). The key decision question for rendering: is the element toggled frequently? Use x-show. Is it shown rarely or does it contain expensive children? Use x-if.

For Hyvä developers, x-cloak is especially relevant: it prevents the so called FOUC (Flash of Unstyled Content), where Alpine.js templates briefly show their raw directive attributes before Alpine.js has initialized. The rule [x-cloak] { display: none !important } in CSS combined with the x-cloak attribute on the component solves the problem completely. In Magento Hyvä templates, this pattern is essential in every production deployment.

Directive Purpose Shorthand Typical Use
x-data Reactive scope n/a Every Alpine component
x-bind Attribute binding :attr class, style, aria-*, disabled
x-on Event listener @event click, input, keydown.escape
x-show display:none toggle n/a Frequently toggled elements
x-if DOM insert/remove on <template> Rare elements, expensive children

Mironsoft

Alpine.js, Hyvä Themes, and Magento 2 frontend development

Need Alpine.js components for your Hyvä shop?

We build performant, accessible Alpine.js components for Magento 2 Hyvä themes, from the minicart to advanced product listings, using the full Alpine.js v3 feature set together with Tailwind CSS v4.

Component Development

Custom Alpine.js components for Hyvä themes, fully reactive and accessible

Code Review

Auditing existing Alpine.js implementations for performance, correctness, and accessibility

Migration

Migrating existing Knockout.js / jQuery solutions to Alpine.js v3 and Hyvä

Alpine.js v3 Directives: The Essentials at a Glance

State & Scope

x-data starts every component. Named components via Alpine.data() and global stores via Alpine.store() keep larger state cleanly organized.

x-show vs. x-if

x-show for frequent toggles (DOM stays), x-if on <template> for rare or expensive elements (DOM gets removed). Picking the wrong one costs performance.

Modifiers

@click.prevent, .stop, .window, .once, .debounce.300ms and x-model.lazy, .number, .trim cut event boilerplate down to a minimum.

Advanced Directives

x-teleport solves z-index problems with modals. x-ignore protects third party widgets. x-cloak prevents FOUC before Alpine initializes.

11. FAQ: Alpine.js v3 Directives

1What is the difference between x-show and x-if?
x-show sets display:none, the element stays in the DOM. x-if on a template element removes it entirely. x-show toggles faster, x-if saves memory for rare, expensive child components.
2Why must x-if sit on a template element?
The template element renders no content. Alpine.js uses it as a wrapper and only inserts the content into the DOM when the condition is true, avoiding any chicken and egg problem.
3What does x-cloak do?
It prevents FOUC: the CSS rule [x-cloak] { display:none } keeps the element hidden until Alpine.js initializes and removes the attribute.
4x-effect vs. $watch: when to use which?
x-effect automatically tracks every property it reads. $watch observes one explicit property and supplies the old and new value. $watch is more precise, x-effect is simpler for straightforward side effects.
5Alpine.store() vs. x-data: when to use which?
Alpine.store() for state shared across components (cart, login, theme). x-data for local component state. Stores are accessible via $store.name from any context.
6What does x-teleport give you?
It renders content at a different DOM location (the end of body for modals, for example) while keeping the Alpine scope connected. It solves z-index and overflow:hidden problems in nested containers.
7:key in x-for: why does it matter?
It gives Alpine.js a stable identifier for DOM node reuse. Without :key, nodes get recreated instead of recycled, which hurts performance and breaks state retention in animated lists.
8Using Alpine.js in Hyvä templates?
Yes. Hyvä uses Alpine.js as its only JS framework. All 15 directives are available in .phtml templates. Always call $hyvaCsp->registerInlineScript() after an inline script.
9What does x-ignore do?
It tells Alpine.js to ignore an element and all of its children. Useful for third party widgets, rich text editors, or server rendered code blocks that manage their own DOM.
10How do you use $refs?
Add x-ref='name' to an element and access it via this.$refs.name in the scope. Useful for setting focus, reading scroll position, or targeting a canvas. Only visible within its own component.