ReactHustle

How to Create Linkable NextJS Tabs using HeadlessUI and TailwindCSS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create Linkable NextJS Tabs using HeadlessUI and TailwindCSS

In this tutorial, you'll learn how to create linkable, bookmarkable, and accessible Tabs component in NextJS by using @headlessui/react package and TailwindCSS/DaisyUI.

What is a Tabs Component? #

Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy.

Why Create Linkable Tabs? #

It's not uncommon to create linkable tabs so that we can save the selected tab state in the URL.

For example, if you have 3 tabs on the /accounts page: profile, subscription, and settings tabs. If a user navigates to the /account/settings route, it will make sense if the "settings" tab is selected. 

Why Use HeadlessUI and TailwindCSS? #

HeadlessUI is a set of unstyled, accessible components that makes it super easy to get complete control of your component. HeadlessUI also provides us with the component structure for a Tabs component, we don't have to implement the Tabs component ourselves, we have to integrate it into NextJS.

When using MUI or Bootstrap, sometimes it's a pain to customize, especially if you need to integrate it with the <Link/> component of NextJS.

TailwindCSS is a set of utility classes, that makes it very easy to integrate into any application even if your primary styling solution is not Tailwind.

Tutorial Objectives  #

In this tutorial, we want to achieve the following:

  1. Create a Tabs component that is accessible and keyboard controls should work. We achieve this out of the box using HeadlessUI.
  2. The tabs component should modify the path. And when navigating to a path, it should select the corresponding Tab.
  3. It should not refresh the page when clicking or changing tabs.
  4. We should be able to style the tabs however we want.

Final Output #

Here's the final output of what we'll be making today.

NextJS Tabs Tutorial Final Output

Step 0 - Project Setup #

If you want to follow along, I created a NextJS template starter that uses tailwind and daisyUI so that we don't have to install it ourselves. You can run the command:

npx create-next-app -e https://github.com/jmarioste/next-daisyui-starter-plain next-tabs-tutorial

After installing, start the local server by running the command:

cd next-tabs-tutorial
yarn dev

After that, let's install the packages that we'll be needing for this tutorial: @headlessui/react & classnames

yarn add @headlessui/react classnames

Step 1 - Defining the page routes #

The next step is to define the page routes. NextJS uses file system routing, and it's important to be able to use the shallow routing feature of NextJS to avoid reloading the page. If we mess it up, we won't be able to use the shallow routing feature since it has its limitations.

I say all that but actually, we just have to use a dynamic route. Create the file /pages/account/[tab].tsx.

// pages/account/[tab].tsx
import React from 'react'
const AccountPage = () => {
  return (
    <div>AccountPage</div>
  )
}
export default AccountPage
12345678

Step 2 - Creating Unstyled Tabs with HeadlessUI #

Next, let's use HeadlessUI to create the unstyled tabs component:

// pages/account/[tab].tsx
import { Tab } from "@headlessui/react";
const AccountPage = () => {
  const tabs = ["profile", "subscription", "settings"];
  return (
    <div>
      <Tab.Group>
        <Tab.List>
          {tabs.map((tab) => (
            <Tab key={tab}>{tab + "/"}</Tab>
          ))}
        </Tab.List>
        <Tab.Panels>
          <Tab.Panel>Profile Content</Tab.Panel>
          <Tab.Panel>Subscription Content</Tab.Panel>
          <Tab.Panel>Settings Content</Tab.Panel>
        </Tab.Panels>
      </Tab.Group>
      {/* use to check the window location in demo */}
      {typeof window !== "undefined" && (
        <pre>Current Location : {window.location.href}</pre>
      )}
    </div>
  );
};
export default AccountPage;
1234567891011121314151617181920212223242526

Explanation:

We use the Tab.Group component as the wrapper for the Tabs.

We use the Tab.List and Tab components to create the buttons that switch the view when clicked.

We use the Tab.Panels and Tab.Panel components to contain the content for each view.

This is almost fully functional. You can click the tab or use the arrow keys to switch the view.

Below is a demo after this step. Go to localhost:3000/account/profile. You can see that the location doesn't change even if we switch tabs. We'll fix it in the next step.

Unstyled NextJS Tabs with HeadlessUI&nbsp;

Step 3 - Integrating next/link and router #

Currently, our tabs component is uncontrolled. Meaning we don't currently control the state inside the Tab.Group component, to add control, we need to provide selectedIndex and onChange props.

Let's integrate NextJS useRouter and Link component to handle the selected state of the Tabs component:

// pages/account/[tab].tsx
import { Tab } from "@headlessui/react";
import Link from "next/link";
import { useRouter } from "next/router";
const AccountPage = () => {
  const tabs = ["profile", "subscription", "settings"];
  // 👇 1. Identify the selected index based on router.query.
  const router = useRouter();
  const _selectedTab = (router.query.tab as string) ?? "profile";
  const selectedIndex = tabs.indexOf(_selectedTab) ?? 0;
  if (!router.isReady) {
    return null;
  }
  return (
    <div>
      <Tab.Group
        // 👇 2. make the Tab.Group component a "controlled" component
        // by providing selectedIndex and onChange
        selectedIndex={selectedIndex}
        onChange={(index) => {
          const tab = tabs.at(index);
          router.replace(`/account/${tab}`, undefined, { shallow: true });
        }}
      >
        <Tab.List className={"tabs tabs-boxed"}>
          {tabs.map((tab) => (
            // 👇 3. use the next/link component as the component for the Tab component.
            // this will add the LinkProps to the tab component like `href`,`shallow`,etc.
            <Tab key={tab} as={Link} href={`/account/${tab}`} shallow>
              {tab}
            </Tab>
          ))}
        </Tab.List>
        ...
      </Tab.Group>
     ...
    </div>
  );
};
export default AccountPage;
12345678910111213141516171819202122232425262728293031323334353637383940

This is the output after this step. Notice that now, the location changes when we switch tabs. Also, try refreshing the different URLs, if it matches the content.

NextJS Tabs Integrated with Link component and router

Step 4 - Adding Styling #

The last step is to add styling to our components so that it looks nicer and more importantly, we can easily identify the selected tab. Let's use some tailwind classes and DaisyUI tab classes here:

// pages/account/[tab].tsx
import { Tab } from "@headlessui/react";
import classNames from "classnames";
import Link from "next/link";
import { useRouter } from "next/router";
const AccountPage = () => {
  const tabs = ["profile", "subscription", "settings"];
  const router = useRouter();
  const _selectedTab = (router.query.tab as string) ?? "profile";
  const selectedIndex = tabs.indexOf(_selectedTab) ?? 0;
  if (!router.isReady) {
    return null;
  }
  return (
    <div className="p-4">
      <Tab.Group
        selectedIndex={selectedIndex}
        onChange={(index) => {
          const tab = tabs.at(index);
          router.replace(`/account/${tab}`, undefined, { shallow: true });
        }}
      >
        <Tab.List className={"tabs tabs-boxed"}>
          {tabs.map((tab, index) => (
            <Tab
              key={tab}
              as={Link}
              href={`/account/${tab}`}
              // 👇 use classNames to toggle the tab-active state easily
              className={classNames({
                "tab m-2 capitalize": true,
                "tab-active": selectedIndex === index,
              })}
              shallow
            >
              {tab}
            </Tab>
          ))}
        </Tab.List>
        <Tab.Panels
          className={"p-2 bg-base-200 my-4 rounded-md text-base-content"}
        >
          <Tab.Panel>Pofile Content</Tab.Panel>
          <Tab.Panel>Subscription Content</Tab.Panel>
          <Tab.Panel>Settings Content</Tab.Panel>
        </Tab.Panels>
      </Tab.Group>
      <pre>Current Location : {window.location.href}</pre>
    </div>
  );
};
export default AccountPage;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152

After this step, you should get this beautiful result:

NextJS Tabs with DaisyUI Styling

That's basically it!

If you need the project full code you can access it at GitHub: NextJS Tabs Tailwind Tutorial

Conclusion #

We learned how to create linkable tabs in NextJS by using the @headlessui/react library and styling it easily using tailwind and daisyUI. I think if we used other styling solutions like CSS-inJS or styled-componentsit will be much harder to integrate everything.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Credits: Image by Joshua Woroniecki from Pixabay

Share this post!

Related Posts