Black Friday: Enjoy a 35% discount on the bundles. Apply the code BLACKFRIDAY35 at checkout! Limited offer.
Hello everyone! This tutorial will show you how to create a persistent tabs component using Alpine JS and Tailwind CSS.
Persistent tabs offer several benefits in web applications:
Persistent tabs can be useful in various scenarios:
By implementing persistent tabs, you can significantly enhance the usability and user-friendliness of your web application across these and many other use cases.
The wrapper is where we’ll add the snippet to persist the tab state.
x-data
is a directive that allows us to create a reactive data object in our component.activeTab
is a property that will hold the current active tab index.setActiveTab
is a method that will be called when the user clicks on a tab.localStorage
is a built-in JavaScript object that allows us to store data locally in the browser.<div x-data="{
activeTab: parseInt(localStorage.getItem('activeTab')) || 0,
setActiveTab(index) {
this.activeTab = index;
localStorage.setItem('activeTab', index.toString());
}
}">
<!-- Tabs goes here -->
</div>
The wrapper is an ul
element:
Attributes
role="tablist"
: Indicates that the element is a tablist.
Classes-mb-px
: Adds a negative margin to the bottom of the element.flex
: Applies flexbox layout to the element.items-stretch
: Stretches the items in the list to fill the available space.text-slate-500
: Sets the text color to a light gray color.<ul role="tablist" class="-mb-px flex items-stretch text-slate-500">
<!-- Tabs go here -->
</ul>
The buttons are li
elements without the need of classes on this example.
<li role="presentation">
<!-- Button goes here -->
</li>
Attributes
role="presentation
: Indicates that the element is a presentational element.Attributes
role="tab"
: Indicates that the element is a tab.id="tab-1"
: Assigns a unique ID to the tab.aria-controls="panel-1"
: Associates the tab with its corresponding panel.:aria-selected="activeTab === 0"
: Dynamically sets the aria-selected attribute based on whether this tab is active.:tabindex="activeTab === 0 ? 0 : -1"
: Makes the tab focusable when active, and not focusable when inactive.Alpine JS Directives
@click="setActiveTab(0)"
: Defines a click event handler to activate this tab.:class="{ 'bg-orange-50 text-orange-600': activeTab === 0 }"
: Applies orange styling to the tab when it’s active.Classes
flex
: Applies flexbox layout to the element.items-center
: Centers the content vertically.rounded-full
: Applies rounded corners to the element.px-6
: Adds padding to the left and right sides of the element.h-10
: Sets the height of the element to 10 pixels.py-2
: Adds padding to the top and bottom of the element.text-sm
: Sets the font size to small.font-medium
: Sets the font weight to medium.focus:outline-none
: Removes the default browser outline when the element is focused.focus:ring-2
: Adds a border-width of 2 pixels to the element when it’s focused.focus:ring-orange-500
: Applies the orange-500 color to the border when the element is focused.<button
role="tab"
id="tab-1"
aria-controls="panel-1"
:aria-selected="activeTab === 0"
:tabindex="activeTab === 0 ? 0 : -1"
@click="setActiveTab(0)"
:class="{
'bg-orange-50 text-orange-600': activeTab === 0
}"
class="flex items-center rounded-full px-6 py-2 h-10 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-orange-500"
>
My account
</button>
The other button is the same, but with a different id
and aria-controls
attribute. This is because the buttons are associated with their respective panels.
The pannels are section
elements where the content of the tabs will be displayed.
Attributes
id="panel-1"
: Assigns a unique ID to the panel.role="tabpanel"
: Indicates that the element is a tabpanel.aria-labelledby="tab-1"
: Associates the panel with the corresponding tab.
Alpine JS Directivesx-show="activeTab === 0"
: Conditionally displays the panel based on the active
Classesp-8
: Adds padding to the panel.<section
id="panel-1"
role="tabpanel"
aria-labelledby="tab-1"
x-show="activeTab === 0"
class="p-8">
Content 1
</section>
Panel 2 is similar to Panel 1, but with a different id
and aria-labelledby
attribute.
As you can see the markup is quite simple.
<div x-data="{
activeTab: parseInt(localStorage.getItem('activeTab')) || 0,
setActiveTab(index) {
this.activeTab = index;
localStorage.setItem('activeTab', index.toString());
}
}">
<!-- Tab List -->
<ul role="tablist" class="-mb-px flex items-stretch text-slate-500">
<!-- Tab 1 -->
<li role="presentation">
<button @click="setActiveTab(0)" :aria-selected="activeTab === 0" :tabindex="activeTab === 0 ? 0 : -1" :class="{ 'bg-orange-50 text-orange-600': activeTab === 0 }" class="flex items-center rounded-full px-6 h-10 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-orange-500" role="tab" id="tab-1" aria-controls="panel-1"> My account </button>
</li>
<!-- Tab 2 -->
<li role="presentation">
<button @click="setActiveTab(1)" :aria-selected="activeTab === 1" :tabindex="activeTab === 1 ? 0 : -1" :class="{ 'bg-orange-50 text-orange-600': activeTab === 1 }" class="flex items-center rounded-full px-6 h-10 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-orange-500" role="tab" id="tab-2" aria-controls="panel-2"> Biling </button>
</li>
</ul>
<!-- Panels -->
<div class="rounded-b-md border rounded-xl overflow-hidden border-neutral-200 mt-2 bg-white">
<!-- Panel 1 -->
<section x-show="activeTab === 0" role="tabpanel" id="panel-1" aria-labelledby="tab-1" class="p-8"> Content 1 </section>
<!-- Panel 2 -->
<section x-show="activeTab === 1" role="tabpanel" id="panel-2" aria-labelledby="tab-2" class="p-8"> Content 2 </section>
</div>
</div>
In this tutorial, we learned how to create a persistent tabs component using Alpine JS and Tailwind CSS, and how to use Alpine JS directives to conditionally display the content of the tabs based on the active tab and save the active tab state in the browser’s local storage.
I hope you found this tutorial helpful and have a great day!
/Michael Andreuzza
Own all themes forever for $199.
Includes new themes, updates, unlimited projects, and lifetime support. — No subscription required!