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-gray-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
"I bought a beautiful theme from Lexington a couple weeks ago. I didn't know Astro at the time, but Michael helped me get set up and really went above and beyond with his support. Now I'm happily redoing my site to look gorgeous with his template."
Stuart
Creator of saasydb.com
"Michael is one of the best designers on Twitter, would highly recommend his Lexington Themes if you want something in tailwind that doesn’t look the same as everyone else!"
Alex Hughes
letsloopin.com
Get lifetime access to every theme available today for $199 and own
them forever. Plus, new themes, lifetime updates, use on unlimited
projects and enjoy lifetime support.
No subscription required!
Beautifully designed HTML, Astro.js and Tailwind themes! Save months of time and build your startup landing page in minutes.