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:
We've moved the accordion data into a JavaScript function, making it easier to manage and extend.
We're using
x-for
to iterate over the accordion items, reducing repetition in our HTML.We've added proper ARIA attributes for improved accessibility.
Tailwind CSS classes are used for styling (you'll need to include Tailwind in your project).
Alpine.js transition directives are used for smooth content transitions.
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:
We've added keyboard event handlers for Enter, Space, Arrow Up, and Arrow Down keys.
The
focusPreviousItem()
andfocusNextItem()
functions handle circular navigation through the accordion headers.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.