DEV Community

loading...

Creating Tabs component in Vue 3

Zafar Alam
・9 min read

Hi! In this post we are going to walk through how to create a Tabs component in Vue 3.

The main aim for this post is for me to get started with writing posts and giving back to the wonderful open source community. If you find this helpful please share and like the post. Also please send you feedback on what could be improved for future posts.

You can access the Demo for the sample app.

You can access the full code of the component and sample app

GitHub logo zafaralam / vue3-tabs

A vue 3 tabs component example

Enough of small talk, lets get to business. We are going to start with creating a blank project using Vite for Vue 3 project. You can read more about getting started with Vite at the docs.

We are going to use typescript for this sample project.

$ yarn create vite tabs-example --template vue-ts
Enter fullscreen mode Exit fullscreen mode

Next, we are going to install the dependencies and run the project.

$ yarn 

# once the above command completes run the project with the below command

$yarn dev
Enter fullscreen mode Exit fullscreen mode

You can access a basic Vue 3 app in you browser using http://localhost:3000/ and it should look like the below screenshot.

image

Your project folder structure should look.

├───node_modules
├───public
│   └───favicon.ico
├───src
│   ├───App.vue
│   ├───main.ts
│   ├───shims-vue.d.ts
│   ├───vite-env.d.ts
│   ├───assets
│   │   └──logo.png
│   └───components
│       └──HelloWorld.vue
├───.gitignore
├───index.html
├───package.json
├───README.md
├───tsconfig.json
├───vite.config.js
└───yarn.lock
Enter fullscreen mode Exit fullscreen mode

Next, we will remove all the code within the App.vue file under the src folder and replace it with the below.

App.vue

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
  components: {},
});
</script>
<template>
  <div class="tabs-example">
    <h1>This is a <b>Tabs</b> example project with Vue 3 and Typescript</h1>
  </div>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Now, we can create a new file under the src/components folder called Tabs.vue. We are going to use scss for our styles so we need a sass dependency for our project. You can install it by

yarn add sass
Enter fullscreen mode Exit fullscreen mode

Note: you will need to stop and start the dev server again yarn dev

Now add the following code to the Tabs.vue file we created earlier.

The component also registers an event listener for keyboard events and can tabs can be changed using Ctrl + [Tab number] e.g.Ctrl + 1

Tabs.vue

<script lang="ts">
import {
  defineComponent,
  onMounted,
  onBeforeUnmount,
  ref,
  watch,
  toRefs,
  h,
  VNode,
} from "vue";

interface IProps {
  defaultIndex: number;
  resetTabs: boolean;
  position: string;
  direction: string;
  reverse: boolean;
}

export default defineComponent({
  name: "Tabs",
  props: {
    defaultIndex: {
      default: 0,
      type: Number,
    },
    resetTabs: {
      type: Boolean,
      default: false,
    },
    direction: {
      type: String,
      default: "horizontal",
      validator(value: string) {
        return ["horizontal", "vertical"].includes(value);
      },
    },
    position: {
      type: String,
      default: "left",
      validator(value: string) {
        return ["start", "end", "center"].includes(value);
      },
    },
    reverse: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
  emits: {
    tabChanged(index: number) {
      return index !== undefined || index !== null;
    },
  },
  setup(props: IProps, { emit, slots, attrs }) {
    const { defaultIndex, resetTabs, position, direction, reverse } =
      toRefs(props);

    const selectedIndex = ref(0);
    const tabs = ref<Array<any>>([]);
    const _tabItems = ref([]);

    const onTabKeyDown = (e: KeyboardEvent) => {
      if (e.ctrlKey || e.metaKey) {
        if (parseInt(e.key) - 1 in tabs.value) {
          e.preventDefault();
          switchTab(e, parseInt(e.key) - 1, tabs.value[parseInt(e.key) - 1]);
        }
      }
    };

    const reset = () => {
      selectedIndex.value = 0;
    };

    const switchTab = (_: any, index: number, isDisabled: boolean) => {
      if (!isDisabled) {
        selectedIndex.value = index;
        emit("tabChanged", index);
      }
    };

    onMounted(() => {
      document.addEventListener("keydown", onTabKeyDown);
    });

    onBeforeUnmount(() => {
      document.removeEventListener("keydown", onTabKeyDown);
    });

    watch(defaultIndex, (newValue, oldValue) => {
      if (newValue !== selectedIndex.value) {
        selectedIndex.value = newValue;
      }
    });

    watch(resetTabs, (newValue, oldValue) => {
      if (newValue === true) reset();
    });

    return () => {
      _tabItems.value = (slots as any)
        .default()
        .filter((component: any) => component.type.name === "Tab");

      const tabList: Array<VNode> = [];
      _tabItems.value.forEach((tab: VNode, index: number) => {
        const _tabProps = tab.props as {
          title?: string;
          "title-slot"?: string;
          disabled?: boolean | string;
        };

        const content = _tabProps["title-slot"]
          ? (slots as any)
              .default()
              .filter(
                (item: any) =>
                  item.type === "template" &&
                  item.props.name === _tabProps["title-slot"]
              )[0].children
          : _tabProps.title;
        const isDisabled =
          _tabProps.disabled === true || _tabProps.disabled === "";
        tabs.value[index] = isDisabled;

        tabList.push(
          h(
            "li",
            {
              class: "tab-list__item",
              tabIndex: "0",
              role: "tabItem",
              "aria-selected": selectedIndex.value === index ? "true" : "false",
              "aria-disabled": isDisabled ? "true" : "false",
              onClick: (e: MouseEvent) => {
                switchTab(e, index, isDisabled);
              },
            },
            content
          )
        );
      });

      const _tabsList = h(
        "ul",
        { class: `tab-list ${position.value}`, role: "tabList" },
        tabList
      );

      return h(
        "div",
        {
          class: `tabs ${direction.value} ${reverse.value ? "reverse" : ""}`,
          role: "tabs",
        },
        [
          _tabsList,
          h("div", { class: "tab" }, _tabItems.value[selectedIndex.value]),
        ]
      );
    };
  },
});
</script>

<style lang="scss">
:root {
  --primary-color: #4313aa;
  --border-color: #e2e2e2;
  --disabled-text-color: #999;
}
.tabs {
  display: grid;
  grid-template-columns: 1fr;
  .tab-list {
    list-style: none;
    display: flex;
    padding-left: 0;
    border-bottom: 1px solid var(--border-color);
    &.center {
      justify-content: center;
    }
    &.end {
      justify-content: flex-end;
    }
    &__item {
      padding: 8px 10px;
      cursor: pointer;
      user-select: none;
      transition: border 0.3s ease-in-out;
      position: relative;
      bottom: -1px;
      text-transform: uppercase;
      font-size: 0.85rem;
      letter-spacing: 0.05rem;

      &:not(:first-child) {
        margin-left: 10px;
      }

      &[aria-selected="true"] {
        border-bottom: 2px solid var(--primary-color);
        font-weight: 700;
        color: var(--primary-color);
      }
      &[aria-disabled="true"] {
        cursor: not-allowed;
        color: var(--disabled-text-color);
      }
    }
  }
  &.horizontal {
    &.reverse {
      .tab-list {
        grid-row: 2;
        border: none;
        border-top: 1px solid var(--border-color);
      }
    }
  }

  &.vertical {
    grid-template-columns: auto 1fr;
    gap: 1rem;
    .tab-list {
      flex-direction: column;
      border-bottom: none;
      border-right: 1px solid var(--border-color);

      &__item {
        margin-left: 0;
        border-radius: 0;

        &[aria-selected="true"] {
          border: none;
          border-left: 2px solid var(--primary-color);
        }
      }
    }

    &.reverse {
      grid-template-columns: 1fr auto;
      .tab-list {
        grid-column: 2;
        border: none;
        border-left: 1px solid var(--border-color);
      }

      .tab {
        grid-row: 1;
        grid-column: 1;
      }
    }
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Next, we will create a Tab.vue file under src/components folder for each of our tabs.

Tab.vue

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "Tab",
  props: ["title", "titleSlot", "disabled"],
});
</script>

<template>
  <div class="tab-panel" role="tabPanel">
    <div class="tab-content"><slot></slot></div>
  </div>
</template>

<style lang="scss">
.tab-panel {
  .tab-content {
    padding: 0.5rem;
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Next we are going to use our newly created components. All examples can be see in the App.vue file. Here I'm going to show you some example use cases.

Example 1

This is the most basic way to use the Tabs component. The tab list will be show at the top and the names of the tabs are derived from the title prop of each Tab component.

<tabs>
    <tab title="Tab 1">
        <h3>This is Tab 1</h3>
    </tab>
    <tab title="Tab 2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 1

Example 2

This example shows that the tab list items can be fully customized with there own icons if required.

<tabs>
    <template name="config">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
        </div>
    </template>
    <tab title-slot="config">
        <h3>This is a config tab</h3>
    </tab>
    <tab title="Tab 2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 2

Example 3

This example shows that the tab list items can be displayed at the bottom using the reverse prop on the Tabs component.

<tabs reverse>
    <template name="tab1">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
        </div>
    </template>
    <template name="tab2">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
        </div>
    </template>
    <tab title-slot="tab1">
        <h3>This is a config tab</h3>
    </tab>
    <tab title-slot="tab2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 3

Example 4

This example shows that the tab list can be shown vertically by using the direction prop on the Tabs component.

<tabs direction="vertical">
    <template name="tab1">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
        </div>
    </template>
    <template name="tab2">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
        </div>
    </template>
    <tab title-slot="tab1">
        <h3>This is a config tab</h3>
    </tab>
    <tab title-slot="tab2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 4

Example 5

This example shows that the tab list can be shown in the center or end by using the position prop on the Tabs component.

<tabs position="center">
    <template name="tab1">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
        </div>
    </template>
    <template name="tab2">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
        </div>
    </template>
    <tab title-slot="tab1">
        <h3>This is a config tab</h3>
    </tab>
    <tab title-slot="tab2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 5

Example 6

This example shows that the tab list can be shown in the center or end by using the position prop on the Tabs component.

<tabs position="end">
    <template name="tab1">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
        </div>
    </template>
    <template name="tab2">
        <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
        </div>
    </template>
    <tab title-slot="tab1">
        <h3>This is a config tab</h3>
    </tab>
    <tab title-slot="tab2">
        <h3>This is Tab 2</h3>
    </tab>
</tabs>
Enter fullscreen mode Exit fullscreen mode

Example 6

App.vue

<script lang="ts">
import { defineComponent } from "vue";
import Tabs from "./components/Tabs.vue";
import Tab from "./components/Tab.vue";

export default defineComponent({
  name: "App",
  components: { Tabs, Tab },
});
</script>
<template>
  <h1>This is a <b>Tabs</b> example project with Vue 3 and Typescript</h1>
  <div class="tabs-example">
    <div class="example example-1">
      <h2>Example 1</h2>
      <p>
        This is the most basic way to use the Tabs component. The tab list will
        be show at the top and the names of the tabs are derived from the title
        prop of each Tab component.
      </p>
      <tabs>
        <tab title="Tab 1">
          <h3>This is Tab 1</h3>
        </tab>
        <tab title="Tab 2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>
    <div class="example example-2">
      <h2>Example 2</h2>
      <p>
        This example shows that the tab list items can be fully customized with
        there own icons if required.
      </p>
      <tabs>
        <template name="config">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
          </div>
        </template>
        <tab title-slot="config">
          <h3>This is a config tab</h3>
        </tab>
        <tab title="Tab 2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>

    <div class="example example-3">
      <h2>Example 3</h2>
      <p>
        This example shows that the tab list items can be displayed at the
        bottom using the <b>reverse</b> prop on the Tabs component.
      </p>
      <tabs reverse>
        <template name="tab1">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
          </div>
        </template>
        <template name="tab2">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
          </div>
        </template>
        <tab title-slot="tab1">
          <h3>This is a config tab</h3>
        </tab>
        <tab title-slot="tab2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>

    <div class="example example-4">
      <h2>Example 4</h2>
      <p>
        This example shows that the tab list can be shown vertically by using
        the <b>direction</b> prop on the Tabs component.
      </p>
      <tabs direction="vertical">
        <template name="tab1">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
          </div>
        </template>
        <template name="tab2">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
          </div>
        </template>
        <tab title-slot="tab1">
          <h3>This is a config tab</h3>
        </tab>
        <tab title-slot="tab2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>

    <div class="example example-4">
      <h2>Example 5</h2>
      <p>
        This example shows that the tab list can be shown in the center or end
        by using the <b>position</b> prop on the Tabs component.
      </p>
      <tabs position="center">
        <template name="tab1">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
          </div>
        </template>
        <template name="tab2">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
          </div>
        </template>
        <tab title-slot="tab1">
          <h3>This is a config tab</h3>
        </tab>
        <tab title-slot="tab2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>

    <div class="example example-4">
      <h2>Example 6</h2>
      <p>
        This example shows that the tab list can be shown in the center or end
        by using the <b>position</b> prop on the Tabs component.
      </p>
      <tabs position="end">
        <template name="tab1">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Config
          </div>
        </template>
        <template name="tab2">
          <div class="tab-title">
            <i class="ri-settings-3-fill" aria-hidden="true"></i>
            Tab 2
          </div>
        </template>
        <tab title-slot="tab1">
          <h3>This is a config tab</h3>
        </tab>
        <tab title-slot="tab2">
          <h3>This is Tab 2</h3>
        </tab>
      </tabs>
    </div>
  </div>
</template>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  text-align: center;
  margin-top: 4px;
}

.tabs-example {
  display: grid;
  place-items: center;
  text-align: left;

  .example {
    width: 80%;
    padding: 0 1rem;
    border-radius: 8px;
    background: #fdfdff;
    border: 2px solid #e7e7f5;
    margin-block-end: 1rem;
  }
}
</style>
Enter fullscreen mode Exit fullscreen mode

As you can see the component can be used in a multitude of ways depending on the need of your app.

I know that the component can be improved and more functionality can be added or improved, so please send in your feedback. Also I will be packaging this component so you can directly use it your own apps without having to write it yourself but I wanted to show you a way of creating dynamic components for your app.

You can access the full code of the component and sample app

GitHub logo zafaralam / vue3-tabs

A vue 3 tabs component example

Thanks for reading and happy coding!!!

Discussion (0)