Lexington has been awarded a grant from Astro, to celebrate. Get a 30% discount. Apply code LEXINGTON30 ( uppercase ) at checkout.

← Back to all tutorials

How to create persistent tabs with Tailwind CSS and Alpine JS

alpine-persistent-tabs
Published and written on Aug 29 2024 by Michael Andreuzza

Hello everyone! This tutorial will show you how to create a persistent tabs component using Alpine JS and Tailwind CSS.

Why persistent tabs?

Persistent tabs offer several benefits in web applications:

  • Improved User Experience: They maintain the user’s last selected tab across page reloads or navigation, providing a seamless and consistent interface.
  • State Preservation: Users don’t lose their place or context when they refresh the page or return to it later.
  • Efficiency: Reduces the need for users to repeatedly select their preferred tab, saving time and reducing frustration.
  • Personalization: Allows for a more tailored user experience by remembering individual preferences.
  • Reduced Cognitive Load: Users don’t have to remember which tab they were on, especially useful for complex applications with multiple sections.

Use cases

Persistent tabs can be useful in various scenarios:

  • Dashboard Applications: Maintain the last viewed section of a multi-faceted dashboard.
  • E-commerce Platforms: Remember the last category a user was browsing in a product catalog.
  • Content Management Systems: Keep track of the last edited section in a multi-step content creation process.
  • Social Media Platforms: Preserve the last viewed tab (e.g., posts, photos, about) on a user’s profile.
  • Educational Platforms**: Maintain the last accessed module or lesson in an online course.
  • Settings Pages: Remember the last settings category a user was modifying.
  • Analytics Tools: Keep the last viewed data set or time range selected by the user.
  • Project Management Tools: Preserve the last project view (e.g., Kanban board, timeline, list view) selected by the user.
  • Documentation Websites: Remember the last viewed section of multi-page documentation.
  • Multi-step Forms: Maintain the last completed step if a user needs to return to the form later.

By implementing persistent tabs, you can significantly enhance the usability and user-friendliness of your web application across these and many other use cases.

Let’s write the markup

The wrapper

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 tablist

The wrapper

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.
  • “: Adds a gap between the items in the list.
  • 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 li element

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.

The Tab Button

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

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 Directives
  • x-show="activeTab === 0": Conditionally displays the panel based on the active Classes
  • p-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.

The full markup

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>

Conclusion

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

Did you like this tutorial? Please share it with your friends!

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!