Creating a Reusable Tab Component in Vue

Johnny Simpson - Jun 19 '22 - - Dev Community

One of the most frequently used UX elements on the web, or on personal devices are tabs. In this guide, let's look at how you can make a reusable tabs component using the Vue Composition API. This set of tabs can be imported, used and styled easily in any project you like, and means you never have to think twice when you want to implement your own set of tabs.

You can find the source code for Vue Tabs on GitHub via this link!

If you're new to Vue, I'd suggest checking out my guide on getting started and making your first Vue app before reading this guide.

Reusable Tab Components in Vue

Creating a Reusable Vue Tabs Component

Tabs essentially consist of two parts - the tab itself, and a container which houses all the tabs. Therefore, to get started, I'm going to make two files in our Vue file structure - Tab.vue and Tabs.vue. Our basic file structure for this component is going to look like this:

|- src
|-- App.vue
|-- main.js
|-- components
|--- Tabs.vue
|--- Tab.vue
|- index.html
|- README.md
|- package.json
Enter fullscreen mode Exit fullscreen mode

So let's start by creating our Tab.vue file. We are using the composition API to make these tabs, so our code is a little simpler than if we used the Options API. You can learn the difference between the Composition and Options API here.

Tab.vue

<script setup>
  import { ref, onMounted } from 'vue';
  const props = defineProps([ 'active' ]);
</script>

<template>
  <div class="tab" :class="(active == 'true') ? 'active' : ''">
    <slot></slot>
  </div>
</template>

<style>
  .tab {
    display: none;
  }
  .tab.active {
    display: block;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

The code for a single tab is relatively simple. Our tabs are going to have one property - active. This property will define whether a tab should show or not. Inside our tab, we put a slot. This is so we can define custom content for our Tab whenever we get round to defining it. Finally, we have some CSS to show or hide tabs, based on if they are active or not.

Now that we have a Tab, let's try making a container for multiple tabs, which I've put in the Tabs.vue file.

Tabs.vue

Let's start by defining our script. The interesting problem we need to solve here is tabs consist of the tabs themselves, and then the tab which you click on to access that particular tab. Therefore, we need to pull our child tabs up, and create headers for each. Let's start by defining our script to do that.

<script setup>
    import { ref, onMounted } from 'vue';
    const props = defineProps([ 'customClass' ]);

    // Defining our reactive `data()` properties
    let tabContainer = ref(null);
    let tabHeaders = ref(null);
    let tabs = ref(null);
    let activeTabIndex = ref(0);

    onMounted(() => {
        tabs.value = [ ...tabContainer.value.querySelectorAll('.tab') ];
        for(let x of tabs.value) {
            if(x.classList.contains('active')) {
                activeTabIndex = tabs.value.indexOf(x);
            }
        }
    });
</script>
Enter fullscreen mode Exit fullscreen mode

In essence, we have to gather our tabs from the tab container through a reference. We'll attach that ref to our template tag later. For now, let's just define the variable. Then we'll need someway to get all the different "tab headers", so let's define that variable now. We'll also need somewhere to store our tabs, which will be in tabs.

Finally, we need a way to track which tab is active, which will be our activeTabIndex. In the composition API, we use ref. If you're familiar with the Options API, most of these variables would've gone in the data() function instead.

When we mount our component, we run onMounted(), and query all tabs. This lets us do two things:

  • We can now get access to all of our tabs, in one simple variable.
  • We can figure out which tab is currently active, and set the variable correctly.

Changing tabs

We'll also need one additional function, for when the user changes tabs. This function just hides all the currently active elements, and then adds active classes to the headers and tabs which are active.

    const changeTab = (index) => {
        // Set activeTabIndex item to the index of the element clicked
        activeTabIndex = index;
        // Remove any active classes
        for(let x of [...tabs.value, ...tabHeaders.value]) {
            x.classList.remove('active')
        }
        // Add active classes where appropriate, to the active elements!
        tabs.value[activeTabIndex].classList.add('active')  
        tabHeaders.value[activeTabIndex].classList.add('active')  
    }
Enter fullscreen mode Exit fullscreen mode

Putting it in our template

Now that we have our script setup, let's make our template and style. Since we've gathered all of our tabs into the tabs variable, we'll loop over it using a v-for. We'll also append on the click event to each of those tab headers.

Note: this is also where we add our references. So our variable, tabContainer, is now tied to #tabs-container, since we added the ref tabContainer to it. The same goes for tabHeaders.

<template>
    <div id="tabs-container" :class="customClass" ref="tabContainer">
        <div id="tab-headers">
        <ul>
            <!-- this shows all of the titles --> 
            <li v-for="(tab, index) in tabs" :key="index" :class="activeTabIndex == index ? 'active' : ''" @click="changeTab(index)" ref="tabHeaders">{{ tab.title }}</li>
        </ul>
        </div>
        <!-- this is where the tabs go, in this slot -->
        <div id="active-tab">
            <slot></slot>
        </div>
    </div>
</template>

<style>
    #tab-headers ul {
        margin: 0;
        padding: 0;
        display: flex;
        border-bottom: 2px solid #ddd;
    }
    #tab-headers ul li {
        list-style: none;
        padding: 1rem 1.25rem;
        position: relative;
        cursor: pointer;
    }
    #tab-headers ul li.active {
        color: #008438;
        font-weight: bold;
    }

    #tab-headers ul li.active:after {
        content: '';
        position: absolute;
        bottom: -2px;
        left: 0;
        height: 2px;
        width: 100%;
        background: #008438;
    }
    #active-tab, #tab-headers {
        width: 100%;
    }

    #active-tab {
        padding: 0.75rem;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Pulling it all together into a single view

Now that we have our two components, we can implement our tabs anywhere we like by importing both and using them. We need to give every Tab a header attribute, which will act as the title for the tab that you click on. Adding tabs to your site then looks like this:

<script setup>
    import Tabs from './components/Tabs.vue'
    import Tab from './components/Tab.vue'
</script>

<template>
    <Tabs>
        <Tab active="true" title="First Tab">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce gravida purus vitae vulputate commodo.
        </Tab>
        <Tab title="Second Tab">
            Cras scelerisque, dolor vitae suscipit efficitur, risus orci sagittis velit, ac molestie nulla tortor id augue.
        </Tab>
        <Tab title="Third Tab">
            Morbi posuere, mauris eu vehicula tempor, nibh orci consectetur tortor, id eleifend dolor sapien ut augue.
        </Tab>
        <Tab title="Fourth Tab">
            Aenean varius dui eget ante finibus, sit amet finibus nisi facilisis. Nunc pellentesque, risus et pretium hendrerit.
        </Tab>
    </Tabs>
</template>
Enter fullscreen mode Exit fullscreen mode

And just like that, we have tabs we can use anywhere. You can view the demo below:

Conclusion and Source Code

Implementing Vue tabs is pretty straightforward, and by importing these two components into any project you'll have instantly functional tabs. You can find the source code for Vue Tabs on GitHub via this link.

I hope you've enjoyed this guide. If you want more, you can find my other Vue tutorials and guides here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .