Tab Component Design With Vue

Luis Ramirez Jr
Luis Ramirez Jr
hero image

Component design is one of the most crucial skills you can pick up as a Vue developer. However, designing a reusable, scalable, and generic component can be a little challenging, and this problem only gets compounded when designing nested components.

The good news?

In this tutorial, I'll help you get past all of that, by walking you through the step-by-step process for designing a tab component.

Huzzah!

So let's dive into it.

Quick setup

This tutorial assumes you're already familiar with the basics of Vue.

For a basic project, you can follow the instructions for new projects here.

Alternatively, you can use StackBlitz to create a new Vue project on the web.

You don't need to install packages like Pinia or the Vue Router. A reasonably minimal project is all that's required.

Feel free to check out this demo for what we'll be building.

Tab component project in Vue

Note: Examples of this component are written with the Options API and composition API.

Preparing the templates

The first step is to create the two components:

  1. Tabs - A container for all tabs, which handles toggling and displaying between them, and
  2. Tab - An individual tab for rendering content

It's always important to consider the responsibility of each component before you create it.

For example

Whenever you're designing nested components, the wrapping component is typically responsible for managing child components, whereas individual child components are responsible for rendering content.

As mentioned earlier, I'll show you how to design a tab component with the options API and composition API, but you can simply follow the examples that suit the API that you'd like to use in your project.

  • TabsOptions.vue - Options API Only
  • TabOptions.vue - Options API Only
  • TabsComposition.vue - Composition API Only
  • TabComposition.vue - Composition API Only

Inside the TabsOptions.vue/TabsComposition.vue component, we'll render the following template:

<template>
  <div>
    <ul>
      <li>Tab Title</li>
    </ul>
    <slot />
  </div>
</template>

The <slot> component is incredibly helpful for rendering content. We'll be using it to render individual tabs inserted into the component's tags.

Next, add a template for the TabOptions.vue/TabComposition.vue components.

<template>
  <div>
    <slot />
  </div>
</template>

Similar to the container, individual tabs can render content with the <slot> component.

Lastly, we can update the App.vue component.

<template>
  <div>
    <h1>Options API</h1>
    <TabsOptions>
      <TabOptions title="Tab 1">Tab 1 Content</TabOptions>
      <TabOptions title="Tab 2">Tab 2 Content</TabOptions>
      <TabOptions title="Tab 3">Tab 3 Content</TabOptions>
      <TabOptions title="Tab 4">Tab 4 Content</TabOptions>
    </TabsOptions>

    <hr />

    <h1>Composition API</h1>
    <TabsComposition>
      <TabComposition title="Tab 1">Tab 1 Content</TabComposition>
      <TabComposition title="Tab 2">Tab 2 Content</TabComposition>
      <TabComposition title="Tab 3">Tab 3 Content</TabComposition>
      <TabComposition title="Tab 4">Tab 4 Content</TabComposition>
    </TabsComposition>
  </div>
</template>

<script>
  import TabsOptions from "@/components/TabsOptions.vue";
  import TabOptions from "@/components/TabOptions.vue";
  import TabsComposition from "@/components/TabsComposition.vue";
  import TabComposition from "@/components/TabComposition.vue";

  export default {
    components: {
      TabsOptions,
      TabOptions,
      TabsComposition,
      TabComposition,
    },
  };
</script>

So let's have a look at whats happening here.

In the above snippet, we're registering the components. (Once again, if you're only interested in a specific API, you don't need to worry about the other components relative to the API you're not using).

In addition to registering the components, we're also using them from within the template to render dummy content.

How?

Well, on the individual tab components, a custom prop called title has been added. The issue of course is that our components don't accept this prop, but they will in the future. We're adding them now just so that we don't have to revisit the App.vue component.

Setting up the container data

The container is going to be responsible for keeping track of the active tab. (Traditionally, tabs only display content from one tab).

Our job is to keep track of which tab to display, and to accomplish this, we must store two pieces of information as data.

  1. First, we must store a complete list of tabs in the current container
  2. Secondly, we must keep track of the active tab

If you're using the options API, add the data() function, with two properties called activeTabHash and tabs in the TabsOptions.vue component.

<script>
  export default {
    data() {
      return {
        activeTabHash: "",
        tabs: [],
      };
    },
  };
</script>

The active tab will be stored as a string, and by default, none of the tabs will be active since the component isn't going to assume there's tab content.

If you're using the composition API, you can create the activeTabHash and tabs properties by using the ref() function in the TabsComposition.vue component.

<script setup>
  import { ref } from "vue";

  const activeTabHash = ref("");
  const tabs = ref([]);
</script>

Passing on data with provide

Displaying content is the responsibility of individual tabs, but there's just one problem - how does a tab know when to display content?

Well, the parent component has the activeTabHash data property to keep track of this information, and we simply pass on this information to each tab.

For this component, we'll be using the provide/inject API. This API allows components to pass on functions and variables to child components.

In the TabsOptions.vue component, we can add the provide() function.

import { computed } from "vue";

export default {
  // Some code here...
  provide() {
    return {
      addTab: (tab) => {
        const count = this.tabs.push(tab);

        if (count === 1) {
          this.activeTabHash = tab.hash;
        }
      },
      activeTabHash: computed(() => this.activeTabHash),
    };
  },
};

A few key points are worth mentioning.

  • The provide() function must be defined as a regular function. Otherwise, you won't have access to the component instance's data. (i.e.: the this keyword)
  • An object must be returned by the provide() function with the data we'd like to expose to child components
  • We have a function called addTab. An object called tab is accepted, which is pushed into the tabs array of the component
  • In the same function, we're checking for how many items there are. If 1 tab has been added, it becomes the active tab by storing its hash. (We'll create the hash in a moment from the respective component)

There's one portion of the code that I want to focus on. Specifically, this line of code.

activeTabHash: computed(() => this.activeTabHash);

It's a strange syntax that you may not have encountered before if you don't use the composition API, so let me explain.

By default, values in the object returned by the provide() function are not reactive. This means if the activeTabHash data property were updated, child components would not be notified of this update.

In most cases though, you'll likely want your data to be reactive for child components. We can achieve this by borrowing the computed() function from Vue, designed for the composition API, and use it to make our data reactive.

import { computed } from "vue";

All you have to do is wrap your data property with this function in an arrow function, and you're good to go!

However, things are different in the TabsComposition.vue component for those of you who are using the composition API.

import { ref, provide } from "vue";

const activeTabHash = ref("");
const tabs = ref([]);

provide("addTab", (tab) => {
  const count = tabs.value.push(tab);

  if (count === 1) {
    activeTabHash.value = tab.hash;
  }
});
provide("activeTabHash", activeTabHash);

We can import the provide function from the vue package to expose the same data and functions to child components.

The function for the composition API is the exact same as the function for the options API, but there are two arguments that must be provided to the provide function.

  1. Firstly, we must set the name of the function
  2. Secondly, we must pass in the value

Unlike the options API, we don't have to wrap the activeTabHash variable with the computed function. This is because it's already made reactive with the ref function.

Looping through the tabs

In the templates of the TabsOptions.vue/TabsComposition.vue, we can update the <li> elements by looping through the tabs array.

<ul>
  <li v-for="tab in tabs" :key="tab.title" @click="activeTabHash = tab.hash">
    {{ tab.title }}
  </li>
</ul>

Binding the key attribute to the tab.title property will help Vue associate each item in the array with an <li> element. Then, from within the element, we're rendering the title with an expression.

Lastly, we're listening to the click event to update the active tab.

This means that if the user clicks on a tab, the activeTabHash property should be updated to the tab's hash, which we'll add soon.

Preparing the tab component data

Time to work on the individual tab components.

First, we must prepare the data in the TabOptions.vue component.

<script>
  export default {
    data() {
      return { hash: "", isActive: false };
    },
  };
</script>

In the data() function, we're creating two properties called hash and isActive.

The hash property will contain a unique ID for the tab, whereas the isActive property will handle toggling the tab's content (This means that initially, the content will be hidden).

In the TabComposition.vue component, we can store the same information with the ref() function.

<script setup>
  import { ref } from "vue";

  const hash = ref("");
  const isActive = ref(false);
</script>

Component data is not the only thing we need to prepare though.

Both tab components accept the title, and so in the TabOptions.vue component, we can add the props object to accept the title prop.

export default {
  props: {
    title: {
      type: String,
      required: true,
    },
  },
  // Some code here...
};

Important: The title prop will be required. Otherwise, Vue will throw an error.

Luckily, we've already prepared titles for each tab in the App.vue component.

In the TabComposition.vue component, we can use the defineProps function to accept props. An array of props must be passed in, and then the props are returned as an object.

const props = defineProps(["title"]);

Injecting data

The parent component exposes a function and data property to child tab components. However, they're not automatically available to our component, and so we must manually accept them.

In the TabOptions.vue component, we can add a property called inject to accept values provided by parent components.

In the example below, we're accepting the addTab and activeTabHash values.

export default {
  inject: ["addTab", "activeTabHash"],
};

For the composition API, we can import the inject() function to accept the same values.

import { inject } from "vue";

const addTab = inject("addTab");
const activeTabHash = inject("activeTabHash");

Registering a tab

Now that we have our data, we must start registering a tab.

At the moment, the parent component is unaware that we have a tab because the tabs array is left unfilled. But by injecting the addTab function, we'll be able to update this array from our component.

For the options API, we'll register the tab during the created() lifecycle.

export default {
  created() {
    this.hash = `#${this.title.toLowerCase().replace(/ /g, "-")}`;

    this.addTab({
      title: this.title,
      hash: this.hash,
    });
  },
  watch: {
    activeTabHash() {
      this.isActive = this.activeTabHash === this.hash;
    },
  },
};

First, we're updating the hash property by converting the title to a hash that we can use in a URL. Keep in mind, your titles should be unique. Otherwise, your hashes won't be unique either.

Next, we're calling the addTab function to register the tab. Two pieces of information are provided, which are the title and hash.

Lastly, we're watching the activeTabHash property for changes. If it changes, we're comparing the active hash with the hash from the current tab. If they're equal, the isActive property will be set to true.

(In the composition API, we can replicate the same behavior by importing the onBeforeMount and watch functions).

import { onBeforeMount, watch } from "vue";

onBeforeMount(() => {
  hash.value = `#${props.title.toLowerCase().replace(/ /g, "-")}`;

  addTab({
    title: props.title,
    hash: hash.value,
  });
});

watch(activeTabHash, () => {
  isActive.value = activeTabHash.value === hash.value;
});

The onBeforeMount() function can be considered the next closest hook to the created() hook for the options API.

Toggling the template

The last step for both components of the options API and composition API is to toggle the content's appearance with the v-show directive.

<template>
  <div v-show="isActive">
    <slot></slot>
  </div>
</template>

And that's it! Now you should be able to click on any of the tabs to toggle content.

Adding Tailwind

You can apply CSS to your components and customize them any way you'd like. But if you want to make things easier, consider using Tailwind.

I've provided a design you can use in your own projects if you'd like.

Check out this guide for installing Tailwind with your project. It works with both local and StackBlitz projects.

For the TabsOptions.vue/TabsComposition.vue components, you can use the following classes:

<template>
  <div class="border-4 border-black rounded">
    <ul class="flex flex-nowrap justify-between">
      <li
        class="w-full font-black text-center py-4 cursor-pointer border-b-4 border-black"
        :class="{
          'bg-yellow-50': tab.hash !== activeTabHash,
          'bg-lime-200': tab.hash === activeTabHash,
        }"
        v-for="tab in tabs"
        :key="tab.title"
        @click="activeTabHash = tab.hash"
      >
        {{ tab.title }}
      </li>
    </ul>
    <slot />
  </div>
</template>

For the TabOptions.vue/TabComposition.vue component, You can add simple padding like so.

<template>
  <div class="p-8" v-show="isActive">
    <slot />
  </div>
</template>

Key takeaways

The biggest challenge in designing nested components is sharing data. Thanks to Vue's provide/inject API, this process is simplified.

  • If you're using the options API, you can leverage provide values with the provide() function and inject values by adding the inject array
  • However, the composition API takes a different approach by importing the provide() and inject() functions into your setup

The key thing to understand is that the behavior is all the same, and I hope this tutorial helped you understand how to design nested components in Vue a little better!

If you want to learn much more about Vue, as well as all thew new Vue 3 features, then check out my complete Vue Developer course.

You can even start taking the course for free (no credit card or signup required) using this link.

learn vue in 2023

It's the only Vue.js tutorial + projects course you need to learn Vue (including all new Vue 3 features), build large-scale Vue applications from scratch, and get hired as a Vue developer this year.

P.S... Want to build a Vue project to make your portfolio stand out?

Check out my new Vue image filters app project (Using Vue, Typescript, and WebAssembly), live over in the ZTM workshops.

More from Zero To Mastery

How To Connect Firebase With Vue preview
How To Connect Firebase With Vue

Firebase + Vue = 😍, but how do you connect the two? It's actually quite easy, once you know how. Let's walk you through it, step-by-step with an example.

How To Auto-Register Components for Vue (With Vite) preview
How To Auto-Register Components for Vue (With Vite)

Not sure when to auto-register components, or how to set it up? Learn the exact steps in today's guide, with a walkthrough example!

PostCSS vs. SASS: Why You Should Use PostCSS With Vue (+ How) preview
PostCSS vs. SASS: Why You Should Use PostCSS With Vue (+ How)

This is why you should consider using PostCSS instead of SASS with Vue and a step-by-step guide of how to use PostCSS with Vue.