Alpine.js x-collapse: Smooth Accordion Animation Without CSS Hacks
AI generated
x-data
Alpine
Alpine.js · x-collapse · Animation · Accordion
Alpine.js x-collapse:
Smooth Accordion Animation Without CSS Hacks

The max-height trick works, but it never really looks fluid. Alpine.js x-collapse animates the actual content height, reacts to dynamic content and integrates seamlessly into Alpine state, without a single line of custom JavaScript.

10 min read x-collapse · x-show · x-transition · Accordion · FAQ Alpine.js 3.x · Hyva Themes

1. The Problem with the max-height Hack

Anyone who wants to animate content in the browser with CSS quickly runs into a fundamental limit: height: auto cannot be animated. CSS transitions only work between two measurable values. The classic workaround is the max-height hack: you set max-height: 0 for the closed state and a generously guessed value like max-height: 1000px for the open state. CSS then animates this maximum value, not the real height. The result is a delayed entrance effect when opening and an abrupt close, because the browser animates from 1000px down to 0, even though the content might only be 200px tall.

In practice this creates two concrete problems. First, if the guessed max-height value is too small, the content gets cut off. If it is too large, a clearly visible easing delay appears, because the animation starts braking from a value far higher than the actual content. Second, with dynamically loaded content, for example an API response that returns more text than expected, the hack fails completely. You would need to set max-height dynamically via JavaScript, which lands you exactly where Alpine.js x-collapse already offers a clean solution.

Many developers reach for jQuery animations with slideDown() and slideUp() at this point. These methods measure the real height and animate it correctly, but they bring jQuery along as a dependency, which is not an option in modern projects such as Hyva Themes that deliberately avoid jQuery. Alpine.js x-collapse fills this gap: real height animation, no jQuery, no custom logic.

2. What x-collapse Does and How It Works Internally

Alpine.js x-collapse is an official plugin shipped as a separate npm package or CDN script. It registers a new directive, x-collapse, which is applied to an element and animates its height based on a boolean Alpine state. Internally, the plugin uses the Web Animations API to use the actually measured content height (scrollHeight) as the animation target. This means that no matter how tall the content really is, the animation always runs exactly to the real boundary.

The plugin also takes care of the overflow: hidden style during the animation, so that no content becomes visible outside the animated area. After the opening animation completes, overflow is reset back to visible, which is important for content that contains, for example, tooltips or dropdowns extending beyond the panel edge. The interplay with x-show is seamless: x-collapse extends the behavior of x-show and replaces the directive's default transition with the measured height animation. The developer does not need any further configuration.

3. Installation and Integration into Alpine.js

The plugin can either be installed via npm or included directly via CDN. In projects with a build process, as is the case in Hyva Themes with Tailwind CSS, the npm variant is the cleaner choice, because it enables tree shaking and requires no external network request. The plugin is imported after Alpine.js and registered before Alpine starts. The order is crucial: Alpine must know about the plugin before it begins DOM initialization.


// main.js: initialize Alpine.js with the x-collapse plugin
import Alpine from 'alpinejs';
import Collapse from '@alpinejs/collapse';

// Register plugin before Alpine starts
Alpine.plugin(Collapse);

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

// CDN alternative (no build process):
// <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
// <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
// Order: x-collapse FIRST, then Alpine (CDN order is reversed relative to defer resolution)

In Hyva Themes, Alpine.js is typically included through the theme's own bundling. The plugin can be added there as an additional import in the hyva-themes/magento2-default-theme configuration. Anyone who prefers the CDN variant needs to pay attention to load order: since both scripts carry defer, the browser loads them in the order of their script tags, so the collapse plugin must appear in the HTML before the main Alpine script, even though it is logically an extension.

4. Building a Simple Accordion with x-collapse

The simplest use case is a single expandable panel. A button controls the boolean state, and a container element carries x-show combined with x-collapse. Nothing more is needed. The result is a fluid height animation when opening and closing that reacts to the actual content height. The button gets @click="open = !open", the panel gets x-show="open" and x-collapse, both directives on the same element.


<!-- Simple expandable panel with x-collapse -->
<div x-data="{ open: false }">
  <button
    @click="open = !open"
    :aria-expanded="open"
    class="flex items-center justify-between w-full px-6 py-4 font-semibold text-left bg-white border border-slate-200 rounded-xl hover:bg-slate-50"
  >
    <span>What is Alpine.js x-collapse?</span>
    <svg
      class="w-5 h-5 transition-transform duration-300"
      :class="{ 'rotate-180': open }"
      fill="none" stroke="currentColor" viewBox="0 0 24 24"
    >
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
    </svg>
  </button>
  <div
    x-show="open"
    x-collapse
    class="overflow-hidden"
  >
    <div class="px-6 py-4 text-slate-600 border border-t-0 border-slate-200 rounded-b-xl bg-slate-50">
      x-collapse is an official Alpine.js plugin that animates height
      transitions using the real scrollHeight, no max-height guessing required.
    </div>
  </div>
</div>

Important: x-collapse and x-show must be on the same element. The plugin internally extends the transition logic of x-show. Anyone who splits the directives across different elements gets no animation. The wrapper element that carries x-collapse should not have its own padding or margin, since these properties affect the height measurement. Instead, give the inner content element the padding.

5. Exclusive Accordion: Only One Panel Open at a Time

A classic accordion, where only one panel can ever be open, requires a shared state on the parent component. Instead of a separate open boolean per panel, you store the index of the currently open panel. Each panel checks whether its index matches the stored one. On click, the index is either set to its own value or reset to null, so clicking an already open panel closes it again.

This pattern can be implemented cleanly with x-for over an array of panel definitions, so no HTML needs to be written separately for each panel. The data is defined in x-data as an array of objects with title and content. This approach is especially valuable for CMS-driven content, where the number of panels varies.

6. Adjusting Animation Speed and Easing

By default, x-collapse uses an animation duration of 250 milliseconds with a simple easing. For most UI contexts this is appropriate, but in some designs, for example very small panels or a deliberately livelier interface, you may want to adjust the duration or the easing. The plugin supports this via the x-collapse.duration.Xms modifier, where X is the desired duration in milliseconds.


<!-- Adjust animation speed with the duration modifier -->
<div x-data="{ open: false }">
  <button @click="open = !open" :aria-expanded="open" class="w-full text-left px-6 py-4 font-semibold">
    Panel with 500ms animation
  </button>

  <!-- Default: 250ms -->
  <div x-show="open" x-collapse>
    <p class="px-6 py-4">Default speed: 250 milliseconds.</p>
  </div>
</div>

<div x-data="{ open: false }">
  <button @click="open = !open" :aria-expanded="open" class="w-full text-left px-6 py-4 font-semibold">
    Slow panel
  </button>

  <!-- Custom: 600ms -->
  <div x-show="open" x-collapse.duration.600ms>
    <p class="px-6 py-4">Slower animation for more dramatic transitions.</p>
  </div>
</div>

<!-- For users with prefers-reduced-motion: disable in CSS -->
<style>
@media (prefers-reduced-motion: reduce) {
  [x-collapse] { transition-duration: 0ms !important; }
}
</style>

The prefers-reduced-motion media query is mandatory for accessible implementations that use animation. Users who have motion reduction enabled in their system settings benefit from instant transitions without delay. Alternatively, this CSS override can be mapped directly as a utility class in the Tailwind configuration.

7. Dynamic Content and Asynchronous Loading

One of the biggest advantages of x-collapse over the max-height hack shows up with dynamically loaded content. If a panel needs to load additional content after first opening, for example through an API request that delivers further details, the height animation must react to the new content size. With the max-height hack you would have to reset the value after loading. With x-collapse this happens automatically, because the plugin re-measures the current scrollHeight on every opening animation.

The pattern for asynchronous loading combines x-collapse with Alpine's fetch integration and a loading state. On first open, the fetch is triggered, the content is written into the state after the response arrives, and Alpine re-renders the panel with the new content. Reopening the panel does not trigger a second fetch if the content is already cached.

8. Accessibility: aria-expanded and Keyboard Navigation

Accessible accordions require more than just visual animation. Screen readers need to be informed about the state of each panel. The aria-expanded attribute on the trigger button communicates the open state. With Alpine.js this is simple: :aria-expanded="open" binds the state directly as an ARIA attribute. Screen readers then announce "expandable, expanded" or "expandable, collapsed" when the user focuses the button.


<!-- Accessible accordion with full ARIA support -->
<div x-data="{ activePanel: null }" class="space-y-2">
  <template x-for="(panel, index) in [
    { id: 'p1', title: 'Shipping time', content: 'We deliver within 2-3 business days.' },
    { id: 'p2', title: 'Returns', content: '30-day return policy, no reason required.' },
    { id: 'p3', title: 'Payment', content: 'Invoice, credit card, PayPal and instant bank transfer.' }
  ]" :key="panel.id">
    <div class="border border-slate-200 rounded-xl overflow-hidden">
      <button
        :id="'trigger-' + panel.id"
        :aria-controls="'panel-' + panel.id"
        :aria-expanded="activePanel === index"
        @click="activePanel = activePanel === index ? null : index"
        class="flex items-center justify-between w-full px-6 py-4 font-semibold text-left hover:bg-slate-50"
      >
        <span x-text="panel.title"></span>
        <span aria-hidden="true" class="text-teal-700 font-mono transition-transform duration-200"
              :class="activePanel === index ? 'rotate-45' : ''" >+</span>
      </button>
      <div
        :id="'panel-' + panel.id"
        :aria-labelledby="'trigger-' + panel.id"
        role="region"
        x-show="activePanel === index"
        x-collapse
      >
        <p class="px-6 py-4 text-slate-600 border-t border-slate-100" x-text="panel.content"></p>
      </div>
    </div>
  </template>
</div>

The role="region" assignment and the link between button and panel via aria-controls/aria-labelledby satisfy the ARIA Authoring Practices for disclosure widgets. Keyboard navigation works automatically, because real <button> elements are focusable via Tab and activatable via Enter/Space by default.

9. x-collapse vs. max-height vs. x-transition Compared

The choice between the three approaches depends on the concrete use case. For simple fade effects without height change, x-transition is sufficient. For height animations of static content with a known maximum height, the max-height hack works tolerably. As soon as the content is dynamic or the animation truly needs to be fluid, there is no way around x-collapse.

Criterion max-height CSS x-transition x-collapse
Real height animation No (guessed value) No (opacity/scale only) Yes (scrollHeight)
Dynamic content Problematic Not relevant Fully supported
No custom JS needed Yes Yes Yes (plugin)
Overflow after animation hidden (change manually) Not affected Automatically visible
Alpine integration External via :class Native Native (plugin)

In practice, x-collapse is recommended for all UI elements where content is visibly shown and hidden: accordions, FAQ sections, product descriptions in e-commerce, filter panels and navigation submenus. The overhead of the plugin is minimal, the compressed CDN version weighs in under 1 KB.

Mironsoft

Alpine.js, Hyva Themes and Magento 2 frontend development

Fluid UI animations for your Magento store?

We implement accessible Alpine.js components with x-collapse, x-intersect and Alpine plugins: performant, without jQuery, and fully integrated into Hyva Themes.

Accordion components

FAQs, product details and filter panels with x-collapse and full ARIA support

Alpine.js plugin setup

Clean integration of x-collapse, x-intersect and Focus into the Hyva build process

Performance audit

Analysis of existing animations, replacing jQuery dependencies with Alpine.js

10. Summary

The Alpine.js plugin x-collapse solves a problem that has existed in frontend development for years: reliable and fluid animation of elements with variable height. Instead of relying on the max-height hack with its known weaknesses or reintroducing jQuery, x-collapse offers a clean, declarative solution directly within Alpine.js. The plugin measures the real scrollHeight, animates to it, and correctly resets the overflow after the animation.

Integration requires just a few lines: register the plugin, write x-collapse next to x-show on the element, done. Accessibility is ensured through aria-expanded and role="region". For animation duration, the duration modifier is available. Exclusive accordions with only one panel open can be elegantly mapped with an index-based state on the parent container. Anyone building accordions, FAQ sections or filter panels in Alpine.js should use x-collapse as the default.

x-collapse: the essentials at a glance

Real height animation

Measures scrollHeight and animates to it: no guessed value, no layout shift, correct even with dynamic content.

Simple syntax

x-show="open" x-collapse on the same element, nothing more is needed for fluid height transitions.

Accessibility

:aria-expanded="open" on the trigger button communicates the state to screen readers, mandatory for accessible accordions.

Duration modifier

x-collapse.duration.600ms adjusts the animation duration. Use prefers-reduced-motion in CSS for accessible motion reduction.

11. FAQ: Alpine.js x-collapse

1What is Alpine.js x-collapse?
Official Alpine.js plugin for height animations: measures scrollHeight and uses the Web Animations API. No max-height guessing, no manual JavaScript.
2x-collapse and x-show on the same element?
Yes, mandatory. x-collapse extends the x-show transition logic internally. Different elements means no animation.
3Installation via npm or CDN?
npm: Alpine.plugin(Collapse) before Alpine.start(). CDN: plugin script before the Alpine script (both defer, order in the HTML decides).
4Adjust animation speed?
x-collapse.duration.Xms, default 250ms. Take prefers-reduced-motion into account via CSS for accessible motion reduction.
5Is dynamically loaded content not a problem?
Correct. scrollHeight is re-measured on every animation. Reloaded content changes the target value automatically.
6Why is the max-height hack problematic?
Animates a guessed value, not the real height. Too small: content is cut off. Too large: visible easing delay. Dynamic content: complete failure.
7Build an exclusive accordion?
activePanel: null in the parent x-data. x-show="activePanel === index". On click: activePanel = activePanel === index ? null : index.
8What happens to overflow after the animation?
During the animation: hidden. After opening: automatically visible. Tooltips and dropdowns inside the panel work correctly.
9Ensure accessibility?
:aria-expanded="open" on the button. role="region" and aria-labelledby on the panel. Real button elements for keyboard navigation.
10How large is the plugin?
Under 1 KB compressed. Minimal performance overhead, fully justified by the much better animation quality compared to CSS hacks.