Creating a Reusable Tab Component in Vue
Tags: typescript, javascript, vue, webdev
Published: 2022-06-19 17:09:25

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](https://github.com/smpnjn/vue-tabs)**!

If you're new to Vue, I'd suggest checking out my guide on getting started and [making your first Vue app](https://fjolt.com/article/vue-create-your-first-app) before reading this guide.

![Reusable Tab Components in Vue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/si0vn60dktfsm10lbr4y.png)
 

## 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
```

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](https://fjolt.com/article/vue-composition-api-vs-options-api).

### Tab.vue
```html
<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>
```

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.
```html
<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>
```

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.
```javascript
    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')  
    }
```

#### 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`.

```html
<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>
```

## 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:
```html
<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>
```

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](https://github.com/smpnjn/vue-tabs).

I hope you've enjoyed this guide. If you want more, [you can find my other Vue tutorials and guides here](https://fjolt.com/category/vue).