Using Tailwind CSS and Alpine.js to Create Animated and Accessible Tabs
Tags: tailwindcss, webdev, tutorial, html
Published: 2023-07-19 09:03:08

####**[Live Demo](https://cruip-tutorials.vercel.app/unconventional-tabs/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/unconventional-tabs/index.html)**

--

Tabs coordinate a variety of use cases and tasks in the interface design. You can see them in action for displaying multiple contents compared to each other or for highlighting the difference between various elements and information in a particular section. We use tabs to group under a specific umbrella 1+ elements that are connected but differ in some ways.

At Cruip, **we have long been fans of tabs** and implemented them in multiple [Tailwind CSS templates](https://cruip.com/). For example, you can see them in use in our [recruitment website template](https://cruip.com/talent/) called **Talent**, or our [elegant HTML website template](https://cruip.com/tidy/) called **Tidy**, or our [SaaS website template](https://cruip.com/open-pro/) called **Open Pro**.

These examples give you a glimpse into how versatile tabs are in interface design and why they are so popular on landing pages or websites.

Ok, as usual, let’s begin by creating the HTML document that will serve as the container for our animated tabs component. We will import **Tailwind CSS** and **Alpine.js** using the CDN. It’s important to note that using the CDN is not recommended for production websites, but for our experiment, it will suffice:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Unconventional Tabs</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Caveat:wght@500&display=swap" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    fontFamily: {
                        inter: ['Inter', 'sans-serif'],
                        caveat: ['Caveat', 'cursive'],
                    },
                },
            },
        };
    </script>
</head>
<body class="relative font-inter antialiased">
    <main class="relative min-h-screen flex flex-col justify-center bg-slate-50 overflow-hidden">
        <div class="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
            <!-- Tabs component -->
        </div>
    </main>
</body>
</html>
```

Building the structure for the tabs component
---------------------------------------------

Now, let’s start building the structure for our tabs component. It will be organized as follows:

```html
<div x-data="{ activeTab: 1 }">
    <!-- Buttons -->
    <div class="flex justify-center">
        <div class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12">
            <!-- Button #1 -->
            <!-- Button #2 -->
            <!-- Button #3 -->
            
        </div>
    </div>
    <!-- Tab panels -->
    <div class="max-w-[640px] mx-auto">
        <div class="relative flex flex-col">
            <!-- Panel #1 -->
            <!-- Panel #2 -->
            <!-- Panel #3 -->
        </div>
    </div>
</div>
```

As you can see, we have already defined a JavaScript object using Alpine.js to manage the state of our component. This object includes a property called `activeTab`, which is initially set to `1`. This value represents the index of the active tab, allowing us to display the corresponding panel.

Adding the buttons
------------------

Now, let’s add the navigation buttons. We’ll begin with the first button and then replicate the structure for the other ones:

```html
<button
    id="tab-1"
    class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
    :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
    @click="activeTab = 1"
>Lassen Peak</button>
```

We have a rather simple HTML structure, styled using Tailwind CSS utility classes. Additionally, we have some JavaScript logic to handle the button’s state. The conditional class (`:class`) dynamically changes the text and background color based on the value of the `activeTab` property. The `@click` event updates the `activeTab` property when the button is clicked.

It’s important to note that when we replicate this structure for the other buttons, we’ll need to update the value of the `activeTab` property based on the button’s index. For instance, the second button should have a value of `2`, and the third button should have a value of `3`. Similarly, update the button’s `id` attribute according to its index.

Building the tab panels
-----------------------

Now, let’s build the panels for our component. We’ll begin with the first panel and then replicate the structure for the remaining panels:

```html
<article
    id="tabpanel-1"
    class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
    x-show="activeTab === 1"
    x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
    x-transition:enter-start="opacity-0 -translate-y-8"
    x-transition:enter-end="opacity-100 translate-y-0"
    x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
    x-transition:leave-start="opacity-100 translate-y-0"
    x-transition:leave-end="opacity-0 translate-y-12"                        
>
    <figure class="min-[480px]:w-1/2 p-2">
        <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-01.jpg" alt="Tab 01" />
    </figure>
    <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
        <div class="flex justify-between mb-1">
            <header>
                <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                <h1 class="text-xl font-bold text-slate-900">Lassen Peak</h1>
            </header>
            <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                    <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                </svg>
            </button>
        </div>
        <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
        <div class="text-right">
            <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
        </div>
    </div>
</article>
```

To ensure smooth animations for the panels entering and exiting the view, we utilized Alpine.js’ `x-transition` directives along with Tailwind CSS classes. The entry animation has a duration of 0.7 seconds (`duration-700`), while the exit animation has a duration of 0.3 seconds (`duration-300`). Additionally, we’ve implemented a custom easing effect to simulate a slight bounce using [arbitrary variants](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-variants) (`ease-[cubic-bezier(0.68,-0.3,0.32,1)]`).

Let’s replicate the structure for the remaining two panels. Make sure to modify the content and update the `x-show` value to display the right panel based on the active tab.

Let’s proceed to complete the missing buttons and panels. The complete code will be as follows:

```html
<div x-data="{ activeTab: 1 }">
    <!-- Buttons -->
    <div class="flex justify-center">
        <div class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12">
            <!-- Button #1 -->
            <button
                id="tab-1"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                @click="activeTab = 1"
            >Lassen Peak</button>
            <!-- Button #2 -->
            <button
                id="tab-2"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 2 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                @click="activeTab = 2"
            >Mount Shasta</button>
            <!-- Button #3 -->
            <button
                id="tab-3"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 3 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                @click="activeTab = 3"
            >Eureka Peak</button>
        </div>
    </div>
    <!-- Tab panels -->
    <div class="max-w-[640px] mx-auto">
        <div class="relative flex flex-col">
            <!-- Panel #1 -->
            <article
                id="tabpanel-1"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                x-show="activeTab === 1"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-01.jpg" alt="Tab 01" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Lassen Peak</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
            <!-- Panel #2 -->
            <article
                id="tabpanel-2"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                x-show="activeTab === 2"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-02.jpg" alt="Tab 02" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Mount Shasta</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
            
            <!-- Panel #3 -->
            <article
                id="tabpanel-3"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                x-show="activeTab === 3"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-03.jpg" alt="Tab 03" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Eureka Peak</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
        </div>
    </div>
</div>
```

This code already provides us with a fully functional component, but there is still much to be done in terms of accessibility. In today’s web development practices, the importance of accessibility is often unknown or underestimated. However, ensuring equal access to information for all individuals is a fundamental aspect that should not be overlooked. In this tutorial, we will focus on enhancing the accessibility of our component, making it more inclusive and user-friendly.

Making the component accessible
-------------------------------

### Using ARIA roles and attributes

**ARIA** stands for “Accessible Rich Internet Applications” and represents a set of roles and attributes that aim to make web applications more accessible to all users. ARIA is supported by all modern browsers and allows us to provide additional information to HTML elements. For instance, we can use ARIA to specify the role or state of elements. This enables screen readers to interpret and convey this extra information to users with disabilities.

To begin, let’s assign the role of `tablist` to the container element that holds the buttons, and assign the role of `tab` to each individual button element:

```html
<div role="tablist" class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12">
    <!-- Button #1 -->
    <button
        id="tab-1"
        role="tab"
        class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
        :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
        @click="activeTab = 1"
    >Lassen Peak</button>
    ...
```

Next, let’s add the role of `tabpanel` to each of the 3 panels:

```html
<article
    id="tabpanel-1"
    role="tabpanel" 
    class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
    x-show="activeTab === 1"
    x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
    x-transition:enter-start="opacity-0 -translate-y-8"
    x-transition:enter-end="opacity-100 translate-y-0"
    x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
    x-transition:leave-start="opacity-100 translate-y-0"
    x-transition:leave-end="opacity-0 translate-y-12"                        
>
    ...
```

Now, in addition to the previously mentioned **ARIA roles**, we need to define some additional **ARIA attributes** for our buttons.

Each button element should have the following attributes:

*   An `aria-selected` attribute, which should be set to `true` if the button is active, and `false` otherwise
*   An `aria-controls` attribute, which should match the `id` of the corresponding panel

```html
<button
    id="tab-1"
    class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
    :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
    :aria-selected="activeTab === 1"
    aria-controls="tabpanel-1"
    @click="activeTab = 1"
    @focus="activeTab = 1"
>Lassen Peak</button>
    ...
```

As you can see, we have used the Alpine.js **x-bind** directive, in the shorthand version `:aria-selected`, to dynamically define the boolean value of the aria-selected attribute.

For the panels, we simply need to add an **aria-labelledby** attribute, where the value is the same as the `id` of the corresponding button. This association allows screen readers to read the description or label to visually impaired users.

```html
<article
    id="tabpanel-1"
    class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
    role="tabpanel" 
    aria-labelledby="tab-1"
    ...
>
```

### Using tabindex attributes

To allow screen readers to navigate from the active tab to its corresponding tab panel, we need to ensure that only the active button has a `tabindex="0"` attribute, while all other buttons have a `tabindex="-1"` attribute. We’ll again use Alpine.js’ `x-bind` directive (`:tabindex`) to dynamically assign these values:

```html
<button
    id="tab-1"
    class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
    :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
    :tabindex="activeTab === 1 ? 0 : -1"
    :aria-selected="activeTab === 1"
    aria-controls="tabpanel-1"
    @click="activeTab = 1"
    @focus="activeTab = 1"
>Lassen Peak</button>
    ...
```

Finally, we will add a `tabindex="0"` to the `tabpanel` to include it in the page’s **Tab** sequence.

```html
<article
    id="tabpanel-1"
    class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
    role="tabpanel" 
    tabindex="0"
    aria-labelledby="tab-1"
    ...
```

### Improving keyboard accessibility

As a final step, we want to ensure that keyboard navigation of our tabs component provides an optimized experience. Currently, due to the custom `tabindex` attributes we’ve applied, the natural keyboard navigation flow is disrupted, and users cannot navigate between the buttons using the `Tab` key. Instead, when the focus is on a `tab`, pressing the `Tab` key moves the focus to the corresponding `tabpanel`.

So, how can we enable navigation between the buttons when one of them is focused? We can achieve this by utilizing the `Left` and `Right` arrow keys! To implement this functionality, we can rely on the helpful [Focus plugin](https://alpinejs.dev/plugins/focus) provided by Alpine.js.

First, ensure that you include the library in the **head** tag of your HTML page:

```html
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
```

Now, we can start using the plugin directives in our code:

```html
<div
    role="tablist"
    class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12"
    @keydown.right.prevent.stop="$focus.wrap().next()"
    @keydown.left.prevent.stop="$focus.wrap().prev()"
>
    <!-- Button #1 -->
    <button ... @focus="activeTab = 1">Lassen Peak</button>
    <!-- Button #2 -->
    <button ... @focus="activeTab = 2">Mount Shasta</button>
    <!-- Button #3 -->
    <button ... @focus="activeTab = 3">Eureka Peak</button>
</div>
```

In summary, we are ensuring the following three things:

*   When the `Right` key is pressed, move the focus to the next button.
*   When the `Left` key is pressed, move the focus to the previous button.
*   When the focus is on one of the buttons, update the value of the `activeTab` variable to display the corresponding `tabpanel`.

But we’re not finished yet! To provide an optimal experience in line with accessibility guidelines, we want users to be able to quickly return to the first tab by pressing the `Home` key, and to the last tab by pressing the `End` key. To achieve this, we will use the `@keydown.home` and `@keydown.end` directives:

```html
<div
    role="tablist"
    class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12"
    @keydown.right.prevent.stop="$focus.wrap().next()"
    @keydown.left.prevent.stop="$focus.wrap().prev()"
    @keydown.home.prevent.stop="$focus.first()"
    @keydown.end.prevent.stop="$focus.last()"
>
    ...
```

Now we’re done! We have created an accessible tabs component optimized for keyboard navigation. Here’s the complete code:

```html
<div x-data="{ activeTab: 1 }">
    <!-- Buttons -->
    <div class="flex justify-center">
        <div
            role="tablist"
            class="max-[480px]:max-w-[180px] inline-flex flex-wrap justify-center bg-slate-200 rounded-[20px] p-1 mb-8 min-[480px]:mb-12"
            @keydown.right.prevent.stop="$focus.wrap().next()"
            @keydown.left.prevent.stop="$focus.wrap().prev()"
            @keydown.home.prevent.stop="$focus.first()"
            @keydown.end.prevent.stop="$focus.last()"
        >
            <!-- Button #1 -->
            <button
                id="tab-1"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 1 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                :tabindex="activeTab === 1 ? 0 : -1"
                :aria-selected="activeTab === 1"
                aria-controls="tabpanel-1"
                @click="activeTab = 1"
                @focus="activeTab = 1"
            >Lassen Peak</button>
            <!-- Button #2 -->
            <button
                id="tab-2"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 2 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                :tabindex="activeTab === 2 ? 0 : -1"
                :aria-selected="activeTab === 2"
                aria-controls="tabpanel-2"
                @click="activeTab = 2"
                @focus="activeTab = 2"
            >Mount Shasta</button>
            <!-- Button #3 -->
            <button
                id="tab-3"
                class="flex-1 text-sm font-medium h-8 px-4 rounded-2xl whitespace-nowrap focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out"
                :class="activeTab === 3 ? 'bg-white text-slate-900' : 'text-slate-600 hover:text-slate-900'"
                :tabindex="activeTab === 3 ? 0 : -1"
                :aria-selected="activeTab === 3"
                aria-controls="tabpanel-3"
                @click="activeTab = 3"
                @focus="activeTab = 3"
            >Eureka Peak</button>
        </div>
    </div>
    <!-- Tab panels -->
    <div class="max-w-[640px] mx-auto">
        <div class="relative flex flex-col">
            <!-- Panel #1 -->
            <article
                id="tabpanel-1"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                role="tabpanel" 
                tabindex="0"
                aria-labelledby="tab-1"
                x-show="activeTab === 1"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-01.jpg" alt="Tab 01" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Lassen Peak</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
            <!-- Panel #2 -->
            <article
                id="tabpanel-2"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                role="tabpanel" 
                tabindex="0"
                aria-labelledby="tab-2"
                x-show="activeTab === 2"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-02.jpg" alt="Tab 02" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Mount Shasta</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
            
            <!-- Panel #3 -->
            <article
                id="tabpanel-3"
                class="w-full bg-white rounded-2xl shadow-xl min-[480px]:flex items-stretch focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300"
                role="tabpanel" 
                tabindex="0"
                aria-labelledby="tab-3"
                x-show="activeTab === 3"
                x-transition:enter="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-700 transform order-first"
                x-transition:enter-start="opacity-0 -translate-y-8"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-[cubic-bezier(0.68,-0.3,0.32,1)] duration-300 transform absolute"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 translate-y-12"                        
            >
                <figure class="min-[480px]:w-1/2 p-2">
                    <img class="w-full h-[180px] min-[480px]:h-full object-cover rounded-lg" width="304" height="214" src="./tabs-image-03.jpg" alt="Tab 03" />
                </figure>
                <div class="min-[480px]:w-1/2 flex flex-col justify-center p-5 pl-3">
                    <div class="flex justify-between mb-1">
                        <header>
                            <div class="font-caveat text-xl font-medium text-sky-500">Mountain</div>
                            <h1 class="text-xl font-bold text-slate-900">Eureka Peak</h1>
                        </header>
                        <button class="shrink-0 h-[30px] w-[30px] border border-slate-200 hover:border-slate-300 rounded-full shadow inline-flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-in-out" aria-label="Like">
                            <svg class="fill-red-500" xmlns="http://www.w3.org/2000/svg" width="14" height="13">
                                <path d="M6.985 1.635C5.361.132 2.797.162 1.21 1.7A3.948 3.948 0 0 0 0 4.541a3.948 3.948 0 0 0 1.218 2.836l5.156 4.88a.893.893 0 0 0 1.223 0l5.165-4.886a3.925 3.925 0 0 0 .061-5.663C11.231.126 8.62.094 6.985 1.635Zm4.548 4.53-4.548 4.303-4.54-4.294a2.267 2.267 0 0 1 0-3.275 2.44 2.44 0 0 1 3.376 0c.16.161.293.343.398.541a.915.915 0 0 0 .766.409c.311 0 .6-.154.767-.409.517-.93 1.62-1.401 2.677-1.142 1.057.259 1.797 1.181 1.796 2.238a2.253 2.253 0 0 1-.692 1.63Z" />
                            </svg>
                        </button>
                    </div>
                    <div class="text-slate-500 text-sm line-clamp-3 mb-2">It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</div>
                    <div class="text-right">
                        <a class="text-sm font-medium text-indigo-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150 ease-out" href="#0">Read more -&gt;</a>
                    </div>
                </div>
            </article>
        </div>
    </div>
</div>
```

Conclusions
-----------

Hopefully, you have enjoyed this tutorial and learned a thing or two that you can apply to your next project to take advantage of tabs to enhance the quality of your interface design. Keep reading if you need to build something similar for **Next.js** or **Vue**: