Consent

This site uses third party services that need your consent.

Skip to content
Steven Roland

Building a Smooth Accordion Component with Alpine.js

Accordions are versatile UI elements that allow users to expand and collapse content sections, making them perfect for FAQs, product details, or any content that needs to be organized into digestible chunks. With Alpine.js, we can create an interactive and accessible accordion component with minimal effort. Let's dive in!

Setting Up Alpine.js

First, make sure you have Alpine.js included in your project. You can add it via CDN by including this script tag in your HTML file:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.min.js" defer></script>

Basic Accordion Structure

Let's start with a basic accordion structure using Alpine.js:

<div x-data="{ activePanel: null }">
  <div class="accordion">
    <div class="accordion-item">
      <h2 @click="activePanel = activePanel === 1 ? null : 1">
        Section 1
      </h2>
      <div x-show="activePanel === 1">
        <p>Content for Section 1</p>
      </div>
    </div>
    <div class="accordion-item">
      <h2 @click="activePanel = activePanel === 2 ? null : 2">
        Section 2
      </h2>
      <div x-show="activePanel === 2">
        <p>Content for Section 2</p>
      </div>
    </div>
    <div class="accordion-item">
      <h2 @click="activePanel = activePanel === 3 ? null : 3">
        Section 3
      </h2>
      <div x-show="activePanel === 3">
        <p>Content for Section 3</p>
      </div>
    </div>
  </div>
</div>

This basic structure uses Alpine.js directives to control the accordion:

  • x-data="{ activePanel: null }" initializes the component's state with no active panel.

  • @click="activePanel = activePanel === 1 ? null : 1" toggles the active panel when a header is clicked.

  • x-show="activePanel === 1" displays the content for the active panel.

Enhancing the Accordion Component

Now, let's enhance our accordion with styling, transitions, and improved accessibility:

<div x-data="accordion()" class="max-w-3xl mx-auto mt-8">
  <div class="accordion">
    <template x-for="(item, index) in items" :key="index">
      <div class="accordion-item border-b border-gray-200">
        <h2>
          <button 
            @click="toggleItem(index)"
            class="flex justify-between items-center w-full py-4 px-6 text-left"
            :aria-expanded="activePanel === index"
            :aria-controls="'panel-' + index"
          >
            <span x-text="item.title" class="text-lg font-semibold"></span>
            <svg 
              class="w-6 h-6 transition-transform duration-200" 
              :class="{'rotate-180': activePanel === index}"
              viewBox="0 0 24 24"
            >
              <path fill="currentColor" d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
            </svg>
          </button>
        </h2>
        <div 
          :id="'panel-' + index"
          x-show="activePanel === index"
          x-transition:enter="transition ease-out duration-300"
          x-transition:enter-start="opacity-0 transform scale-y-90"
          x-transition:enter-end="opacity-100 transform scale-y-100"
          x-transition:leave="transition ease-in duration-300"
          x-transition:leave-start="opacity-100 transform scale-y-100"
          x-transition:leave-end="opacity-0 transform scale-y-90"
          class="px-6 pb-4"
        >
          <p x-text="item.content"></p>
        </div>
      </div>
    </template>
  </div>
</div>
<script>
  function accordion() {
    return {
      activePanel: null,
      items: [
        { title: 'Section 1', content: 'Content for Section 1' },
        { title: 'Section 2', content: 'Content for Section 2' },
        { title: 'Section 3', content: 'Content for Section 3' }
      ],
      toggleItem(index) {
        this.activePanel = this.activePanel === index ? null : index;
      }
    }
  }
</script>

Let's break down the enhancements:

  1. We've moved the accordion data into a JavaScript function, making it easier to manage and extend.

  2. We're using x-for to iterate over the accordion items, reducing repetition in our HTML.

  3. We've added proper ARIA attributes for improved accessibility.

  4. Tailwind CSS classes are used for styling (you'll need to include Tailwind in your project).

  5. Alpine.js transition directives are used for smooth content transitions.

  6. We've added an icon that rotates when the panel is expanded.

Adding Keyboard Navigation

To make our accordion fully accessible, let's add keyboard navigation:

<div x-data="accordion()" class="max-w-3xl mx-auto mt-8">
  <div class="accordion">
    <template x-for="(item, index) in items" :key="index">
      <div class="accordion-item border-b border-gray-200">
        <h2>
          <button 
            @click="toggleItem(index)"
            @keydown.enter="toggleItem(index)"
            @keydown.space.prevent="toggleItem(index)"
            @keydown.arrow-up.prevent="focusPreviousItem(index)"
            @keydown.arrow-down.prevent="focusNextItem(index)"
            class="flex justify-between items-center w-full py-4 px-6 text-left focus:outline-none focus:ring-2 focus:ring-blue-500"
            :aria-expanded="activePanel === index"
            :aria-controls="'panel-' + index"
          >
            <span x-text="item.title" class="text-lg font-semibold"></span>
            <svg 
              class="w-6 h-6 transition-transform duration-200" 
              :class="{'rotate-180': activePanel === index}"
              viewBox="0 0 24 24"
            >
              <path fill="currentColor" d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
            </svg>
          </button>
        </h2>
        <div 
          :id="'panel-' + index"
          x-show="activePanel === index"
          x-transition:enter="transition ease-out duration-300"
          x-transition:enter-start="opacity-0 transform scale-y-90"
          x-transition:enter-end="opacity-100 transform scale-y-100"
          x-transition:leave="transition ease-in duration-300"
          x-transition:leave-start="opacity-100 transform scale-y-100"
          x-transition:leave-end="opacity-0 transform scale-y-90"
          class="px-6 pb-4"
        >
          <p x-text="item.content"></p>
        </div>
      </div>
    </template>
  </div>
</div>
<script>
  function accordion() {
    return {
      activePanel: null,
      items: [
        { title: 'Section 1', content: 'Content for Section 1' },
        { title: 'Section 2', content: 'Content for Section 2' },
        { title: 'Section 3', content: 'Content for Section 3' }
      ],
      toggleItem(index) {
        this.activePanel = this.activePanel === index ? null : index;
      },
      focusPreviousItem(currentIndex) {
        const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.items.length - 1;
        this.$nextTick(() => {
          this.$el.querySelectorAll('button')[prevIndex].focus();
        });
      },
      focusNextItem(currentIndex) {
        const nextIndex = currentIndex < this.items.length - 1 ? currentIndex + 1 : 0;
        this.$nextTick(() => {
          this.$el.querySelectorAll('button')[nextIndex].focus();
        });
      }
    }
  }
</script>

In this final version:

  1. We've added keyboard event handlers for Enter, Space, Arrow Up, and Arrow Down keys.

  2. The focusPreviousItem() and focusNextItem() functions handle circular navigation through the accordion headers.

  3. We've added focus styles to improve visibility for keyboard users.

Conclusion

With Alpine.js, we've created an accordion component that's interactive, visually appealing, and fully accessible. This component can be easily integrated into any project, providing a smooth user experience with minimal JavaScript.

Alpine.js's declarative syntax allows us to create complex UI components with ease, right in our HTML. By leveraging its directives and combining them with proper HTML structure and CSS, we can build powerful, responsive UI elements that enhance our web applications.

Remember, when implementing accordions, always consider accessibility and user experience. Properly implemented accordions can significantly improve content organization and navigation in your web applications, especially for mobile users or when dealing with large amounts of information.

More posts

How to Register Global Functions in PHP Using Composer

Learn how to register global functions in PHP using Composer. This guide covers creating a helpers file, configuring Composer, updating the autoloader, and using global functions. Best practices and tips for efficient implementation are also discussed.

The Evolution of Design Systems in Web Development

Explore the evolution of web design systems, from inline styles to Tailwind CSS. Learn how to create a robust design system using Tailwind's utility classes and @apply directive, balancing flexibility and maintainability for consistent, scalable web applications.

Building a Dynamic Search Component with Alpine.js

Learn to create a dynamic search component using Alpine.js. This tutorial covers basic implementation, real-time filtering, debounce functionality, loading states, and keyboard navigation. Build an efficient and user-friendly search interface with minimal JavaScript.