David Grzyb

Apr 22, 2020

Building Nav Tabs with Tailwind and Alpine.js Tutorial

Lately I've been focusing on Tailwind and Alpine.js a lot, mostly because they are so easy to prototype with and they make building functional frontends for Laravel backends a breeze. I couldn't find a good example for how to make nav tabs work like they do in Bootstrap when I was first figuring out Alpine, so below is what I came up with.

Update: I recorded a video explaining how I built this, check it out here!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus et feugiat neque. Etiam tempus pellentesque rutrum. In hac habitasse platea dictumst. Ut non tristique metus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Mauris vehicula dui finibus augue dictum fringilla. Maecenas sed enim non nisl molestie pretium. Nam nibh ipsum, convallis vel erat a, feugiat luctus purus. Quisque a tempor mauris. Integer scelerisque, urna in sodales mattis, metus nisl iaculis magna, dignissim convallis ipsum ligula et odio.
Mauris tincidunt ligula at lorem commodo consequat. Pellentesque a ex sed massa commodo dapibus vel nec elit. Duis non purus dignissim, iaculis magna ut, lacinia velit. Aenean sollicitudin viverra sodales. Mauris tempor dolor ut ornare dignissim. Pellentesque in malesuada nulla. Suspendisse eleifend erat sit amet dolor sollicitudin, et rhoncus magna porttitor. Duis sed enim nec libero porttitor laoreet eget eget eros. Proin bibendum libero sit amet lacus tristique, et pulvinar nisi hendrerit. Ut ut hendrerit ex, volutpat sollicitudin arcu. Integer suscipit quam eget scelerisque gravida. Curabitur porttitor porttitor rutrum. Sed ac lorem aliquet tortor molestie fermentum. Morbi sodales velit sed tortor facilisis, non eleifend quam ultricies. Ut ornare suscipit velit ac posuere. Cras hendrerit elit et felis mollis ultricies.
Cras nec faucibus velit. Ut ultricies elit neque, scelerisque luctus nisl tempus sed. Morbi posuere vulputate accumsan. Mauris consectetur tristique efficitur. Nunc quis vulputate lacus, in cursus mauris. Fusce iaculis velit ipsum, sed condimentum lectus convallis in. Phasellus sit amet risus sit amet tellus tristique pretium eget id felis. Vivamus pharetra, justo eu rhoncus tempus, ante purus interdum mi, bibendum pulvinar lacus dolor in tortor. Proin lorem magna, imperdiet vel iaculis eget, cursus eget purus. Cras et eleifend nulla. Nam vel sapien id purus sodales tempor nec ultricies justo. Maecenas a luctus eros. Nullam malesuada enim sit amet ex malesuada, nec egestas metus volutpat. Duis gravida dolor in nunc egestas, eu elementum ligula vehicula. Maecenas laoreet congue magna, vel posuere risus elementum in. Sed id arcu ipsum.

You can see a live demo of a similar tab example here.


The easiest way to install Alpine is to just include the script tag below in your HTML, but you can install it with NPM as well. I typically only install with NPM when using Laravel Mix for something like this blog.

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>

We also need Tailwind, which I also just use a script tag for. If you are not prototyping, installing Tailwind with NPM is a better choice as it allows you to use tools such as PurgeCSS to optimize your site.

<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">

As a starting point, your HTML should look something like this, and the page should be blank.

Adding Tabs

I used the tabs from the Tailwind docs which can be found here. These are nice and simple while also being very similar to what Bootstrap offers. I took this example, and modified it slightly by adding padding (line 10 below), and remove the disabled tab button. In this example, I'll be creating a 3 tab design, with different content in each tab.

But where will the actual content be placed? I've added a div with 3 inner divs starting on line 23. These divs contain the different text snippets we want to switch between using the corresponding tabs. The containing div has a w-full class added to it to make sure the tabs take the full width of the tabs' nav bar. To check if the width is what you expect it be, you can open dev tools and check there or you can add a background color like bg-gray-100 that will make the size very obvious on the white background.

As you can see, all of the content divs show up, and that's obviously going to happen because there is no interactivity built in (yet). The basic premise that this works on is simply hiding the tabbed content that we don't want to see. This means keeping track of which tab is selected and only showing the active tab's content.

Declaring a Component Scope

How do we track the opened tab? First we need to actually create a component. With Alpine, this is done by declaring a component scope on the HTML element that contains all parts of the component you're building. This is all done inline, which is what makes Alpine.js so great for prototyping.

Defining the scope is done with x-data. When this is added to an HTML element, it allows you to define the data that will be available inside of your component. In our case, this is where we'll keep track of the current tab. Below is an example of this without the Tailwind classes, so that it's easier to understand.

Setting Data on Click

The value of openTab is now available to us inside of the component, and the default value is 1. You could use any type of index you prefer, and potentially for hash links it would make sense to use strings so the hash link can be pointed to a specific tag. A problem for another time! 😁

Our next issue is setting the value of openTab when a tab is clicked. We can do this by using @click.

Displaying the Correct Content

When a tab is clicked, the value of openTab will now be set to the value specified inside of @click. We can't actually see that it's being set, although we could check that by using our browser's console. For the purpose of this tutorial, we'll just continue to the next step where we show or hide the corresponding tabs.

To do this, we'll use another Alpine.js directive called x-show. The way it works is by toggling display: none; depending on what the directive's expression evaluates to. In our case, we want to evaluate whether the current tab is the tab that should be open by using the indexes we added to the click events.

Here is a working example with some added padding and updated tab titles:

Fixing Tab Styles

As you can see, the tabs work! When a tab is clicked, the correct tab content is displayed. But what about the styling of the tab buttons? When a tab is clicked and active, it should be shown as the active tab. This can be fixed by using :class to evaluate whether the current tab is active and adding the appropriate classes.

Here is the final version:


This code looks messy because of how many classes are used to style the tab elements, so it could benefit from a refactoring. More specifically, the active and inactive tab classes can be stored in the component data and then referenced. This would look something like this:


In the future I'll be adding hash link support. That would basically check for a hash link on page load and assign it to the default tab index before the tab component loads (only if that tab index exists). I hope this is helpful! 😌