Creating Tabs component in Vue 3

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…


This content originally appeared on DEV Community and was authored by Zafar Alam

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.

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

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

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

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>

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

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>

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>

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>

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>

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>

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>

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>

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>

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>

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

GitHub logo zafaralam / vue3-tabs

A vue 3 tabs component example

Thanks for reading and happy coding!!!


This content originally appeared on DEV Community and was authored by Zafar Alam


Print Share Comment Cite Upload Translate Updates
APA

Zafar Alam | Sciencx (2021-08-01T09:42:32+00:00) Creating Tabs component in Vue 3. Retrieved from https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/

MLA
" » Creating Tabs component in Vue 3." Zafar Alam | Sciencx - Sunday August 1, 2021, https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/
HARVARD
Zafar Alam | Sciencx Sunday August 1, 2021 » Creating Tabs component in Vue 3., viewed ,<https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/>
VANCOUVER
Zafar Alam | Sciencx - » Creating Tabs component in Vue 3. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/
CHICAGO
" » Creating Tabs component in Vue 3." Zafar Alam | Sciencx - Accessed . https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/
IEEE
" » Creating Tabs component in Vue 3." Zafar Alam | Sciencx [Online]. Available: https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/. [Accessed: ]
rf:citation
» Creating Tabs component in Vue 3 | Zafar Alam | Sciencx | https://www.scien.cx/2021/08/01/creating-tabs-component-in-vue-3/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.