Using the Alpine Plugins x-intersect and x-collapse in the Hyva Theme
AI generated
Hyvä
phtml
Hyva Theme, Alpine.js, Plugins
Alpine Plugins x-intersect and x-collapse
Official directives instead of hand rolled observer code

A custom IntersectionObserver for lazy loading or a hand written height transition for an accordion are quick to type, and just as quick to get wrong. The official Alpine plugins x-intersect and x-collapse solve exactly these recurring problems declaratively, tested, and CSP friendly, once you wire them into a Hyva theme correctly.

11 min read x-intersect x-collapse Intersection Observer

1. When plain x-data code is enough, and when a plugin pays off

For simple, one off interactions like toggling a CSS class on click, plain x-data code remains the right choice, an extra plugin would just be overhead there. But once browser APIs like the IntersectionObserver, or recurring, easily mishandled animation logic come into play, reaching for an official Alpine plugin pays off, because the core team's implementation covers edge cases that are easy to miss in a hand rolled version.

Typical edge cases in hand rolled IntersectionObserver code are missing cleanup when a component is removed from the DOM, which leads to memory leaks, or miscalculated root margin values with nested scroll containers. x-intersect encapsulates these pitfalls, and x-collapse solves the problem of smoothly animating dynamic heights without fixed pixel values, something plain CSS transition: height only handles with awkward workarounds.

2. x-intersect basics: syntax and modifiers

x-intersect runs an expression exactly when the element the directive is attached to enters the visible viewport, implemented internally through a managed IntersectionObserver. The .once modifier makes the expression fire only on the first entry, after which the observer automatically unsubscribes, ideal for one off actions like lazy loading.

The .threshold modifier controls what percentage of the element must be visible before the expression fires, and .margin adds extra buffer, for instance to start preloading an image just before it actually becomes visible. Together, both modifiers replace manually configuring a rootMargin and threshold object that you would otherwise have to write by hand when calling IntersectionObserver directly.


<div x-data
     x-intersect.threshold.50.margin.200px="visible = true"
     x-data="{ visible: false }">
  <img :src="visible ? '/media/catalog/product/img.jpg' : placeholderSrc"
       loading="lazy" alt="Product image">
</div>

3. Practical example: lazy loading images and components

For product images below the initial viewport, the native loading="lazy" attribute, evaluated by the browser itself, is often enough on its own. x-intersect becomes relevant once accompanying logic needs to fire alongside the image, for instance loading a heavier comparison widget or initializing star rating rendering only once the component is actually in view.

In the following example, an entire product review component is loaded via Ajax only once it enters the viewport, instead of being loaded during the initial page build. That noticeably reduces the number of Ajax calls needed for the first render, especially on long category pages with many product tiles.


function reviewWidget(productId) {
  return {
    loaded: false,
    reviews: [],
    init() {
      // x-intersect calls loadReviews() only once actually visible
    },
    loadReviews() {
      if (this.loaded) return;
      this.loaded = true;
      fetch(`/rest/V1/products/${productId}/reviews`)
        .then((response) => response.json())
        .then((data) => { this.reviews = data; });
    },
  };
}

4. Infinite scroll on the category page with x-intersect

A sentinel element at the end of the product list, bound to x-intersect.half, triggers loading the next product page once it becomes half visible. It is important to move the sentinel to the new end of the list after every load, otherwise the directive only fires once and infinite scroll stops after the second page.

For Magento category pages it is worth adding a server side limit on how many pages get loaded, plus a clearly visible loading indicator, since customers otherwise cannot tell whether more products are actually coming or the end of the list has already been reached. Silent, invisible loading with no feedback is especially confusing on a slow connection.


function categoryInfiniteScroll(baseUrl) {
  return {
    page: 1,
    loading: false,
    finished: false,
    loadNextPage() {
      if (this.loading || this.finished) return;
      this.loading = true;
      fetch(`${baseUrl}&p=${this.page + 1}`)
        .then((response) => response.json())
        .then((data) => {
          this.page += 1;
          this.finished = data.items.length === 0;
          this.loading = false;
        });
    },
  };
}

5. x-collapse basics: smooth height transitions

x-collapse animates showing and hiding an element based on its actual height, instead of abruptly toggling display: none. The plugin measures the element's current scrollHeight at runtime and animates cleanly between values, which is noticeably more robust for variable, dynamically generated content, such as product descriptions of differing length, than a hard coded max-height in Tailwind.

The difference from a plain x-show solution with a CSS transition lies exactly in this height calculation: x-show with an opacity or transform transition visually hides content but, depending on configuration, still occupies space or jumps abruptly, while x-collapse animates the space itself smoothly and then correctly removes the element from document flow.


<div x-data="{ expanded: false }">
  <button type="button" @click="expanded = !expanded" :aria-expanded="expanded">
    Product description
  </button>
  <div x-show="expanded" x-collapse.duration.300ms>
    <p><?= $block->escapeHtml($product->getDescription()) ?></p>
  </div>
</div>

6. Use cases: faceted filters, FAQ accordions, mobile submenus

In faceted navigation, x-collapse makes long attribute lists like size or color expand and collapse cleanly, without the rest of the filter area jumping abruptly when a section opens. For an FAQ accordion on a category page or in additional product information, the same plugin solves exactly the same problem, with the same attribute written once.

Nested mobile navigation menus benefit too, since submenus correctly push the surrounding container's height when opening, instead of overlapping content below. Because x-collapse measures the actual content height at runtime, it also works reliably across translations with substantially different text length, without maintaining a separate height per language.

7. CSP compliant integration: no CDN, bundled locally

Alpine plugins must never be loaded from a CDN via a script tag in Hyva, since that would violate both the Content Security Policy and the principle of not pulling in extra external dependencies. Instead, @alpinejs/intersect and @alpinejs/collapse are added as npm packages in package.json and folded into the theme's bundled JavaScript through the existing build process.

After installation, the plugins are imported ahead of the Alpine core and registered via Alpine.plugin() before Alpine.start() is called. That order matters: if a plugin is registered only after start, x-intersect or x-collapse attributes already present in the DOM will not take effect, because Alpine does not know their directives during its initial scan.


import Alpine from 'alpinejs';
import intersect from '@alpinejs/intersect';
import collapse from '@alpinejs/collapse';

Alpine.plugin(intersect);
Alpine.plugin(collapse);

document.addEventListener('alpine:init', () => {
  // Store and component registrations go here
});

window.Alpine = Alpine;
Alpine.start();

8. CSP compatibility: what to watch for in plugins generally

The official Alpine core plugins such as intersect, collapse, focus, and persist use only standard browser APIs and no eval() or new Function() whatsoever, which lets them run without issue under a strict CSP with no unsafe-eval, exactly what Hyva requires. For third party plugins outside the official Alpine ecosystem, it is always worth checking the source for dynamic code evaluation before adopting them.

A simple practical test is opening the theme in a browser with the CSP active and watching the console for Content Security Policy violations while deliberately triggering every newly added plugin feature once. If the console stays clean, the plugin is fit for production use in a CSP hardened Hyva theme.

9. A custom Alpine.directive() when no plugin fits

Not every use case has a matching official plugin. For recurring, project specific logic needed identically in several places across the theme, such as automatically focusing a field when a modal appears, a custom directive registered through Alpine.directive() is worth writing, instead of copying the same x-data logic in ten different places.

A custom directive is registered exactly like a core plugin before Alpine.start() and gets access to the element as well as the expression, modifiers, and reactive effects through the same API the official plugins use. That is the right middle ground between copied x-data code and a fully maintained, standalone npm package for a single, small piece of functionality.


Alpine.directive('autofocus-on-show', (el, { expression }, { effect, evaluate }) => {
  effect(() => {
    if (evaluate(expression)) {
      requestAnimationFrame(() => el.focus());
    }
  });
});
// Usage: <input x-show="modalOpen" x-autofocus-on-show="modalOpen">
Approach Performance Code effort Maintainability Recommendation
x-intersect (plugin) Efficient, one shared observer internally Very low, declarative attribute High, maintained by the Alpine core team Default choice for visibility triggers
Custom IntersectionObserver Depends on your own implementation High, including cleanup logic Low, has to be maintained by you Only for very specific requirements
x-collapse (plugin) Smooth, measures height at runtime Very low, a single attribute High, maintained by the Alpine core team Default choice for accordions and filters
x-show with a CSS transition Good with a fixed height Medium, transition tuned manually Medium, error prone with dynamic content Only when element height is known and fixed
Custom Alpine.directive() Depends on your own implementation Medium, written once centrally High with good encapsulation For recurring, project specific logic

Mironsoft

Hyvä theme development and Luma migration

Still running Luma, or a Hyvä theme that just doesn't feel right?

We build Hyvä themes for Magento from scratch or migrate existing Luma shops cleanly, with Tailwind CSS, Alpine.js, and none of the unnecessary JavaScript baggage.

Luma-to-Hyvä Migration

Move an existing shop to Hyvä in a structured way, without losing functionality.

Custom Theme Development

Build a custom Hyvä theme from scratch based on your design.

Performance Optimization

Improve Core Web Vitals and load times in the Hyvä frontend with purpose.

10. Summary

Alpine Plugins in Hyva

Plugins instead of hand rolled observer code

x-intersect and x-collapse encapsulate known pitfalls like memory leaks and imprecise height calculations that are easy to miss when rolling your own.

Lazy loading and infinite scroll, declaratively

A single x-intersect attribute replaces manually configured IntersectionObserver instances for images, widgets, and paginated category pages.

CSP friendly without a CDN

Official Alpine plugins use no eval() calls and are bundled locally via npm, instead of being loaded from an external CDN.

A custom directive as a middle ground

For project specific, recurring logic with no matching official plugin, a custom Alpine.directive() is the cleaner alternative to copied code.

11. FAQ: Alpine Plugins in Hyva

1What does x-intersect actually do under the hood?
x-intersect internally manages an IntersectionObserver for the bound element and runs the given expression once the element enters the visible viewport. Modifiers like .once, .threshold, and .margin control the observer's exact behavior.
2When should I use x-intersect instead of a custom IntersectionObserver call?
Almost always, whenever the behavior matches a standard case like lazy loading or infinite scroll. The plugin covers edge cases, such as proper cleanup on DOM removal, that are easy to miss in hand rolled code.
3How does x-collapse differ from x-show with a CSS transition?
x-collapse measures the actual content height at runtime and animates smoothly between values, while x-show with a CSS transition usually assumes a fixed height and looks abrupt or miscalculates with dynamic content.
4Are Alpine plugins like x-intersect and x-collapse compatible with Hyva's CSP?
Yes, the official Alpine core plugins use only standard browser APIs with no eval() or new Function(), so they run without issue under a strict Content Security Policy with no unsafe-eval.
5Am I allowed to load Alpine plugins via a CDN script tag?
No, Hyva does not load external CDN scripts. Plugins must be installed as npm packages and folded into the locally bundled JavaScript through the existing build process.
6How do I register an Alpine plugin correctly so it actually takes effect in the DOM?
The plugin must be registered via Alpine.plugin() before Alpine.start() is called. If registration happens afterward, Alpine ignores x-intersect or x-collapse attributes already present in the DOM.
7What is x-collapse particularly well suited for in a Hyva theme?
Faceted filter lists, FAQ accordions, and mobile submenus with variable content length, because the plugin measures height at runtime and therefore also works reliably across translations of differing length.
8When is a custom Alpine.directive() worth it over an official plugin?
When project specific, recurring logic is needed identically in several places across the theme and no official plugin exists for it, such as automatically focusing a field when a modal opens.
9Does x-intersect hurt page performance when used on many elements?
No, generally not, since the plugin relies on an efficient, shared observer mechanism. Many separate, manually created IntersectionObserver instances with no shared management would be far more problematic.
10How do I test whether a new Alpine plugin triggers CSP violations?
Open the theme in a browser with the CSP active, watch the console, and deliberately trigger every newly added plugin feature once. If the console stays free of Content Security Policy messages, the plugin is safe to use.