Easy yet flexible way to display child routes in tabs with Vue 3
Tags: vue, ui
Published: 2023-05-24 21:44:18

---
title: Easy yet flexible way to display child routes in tabs with Vue 3
published: true
date: 2023-05-09 18:22:17 UTC
tags: vue, UI
canonical_url: https://bytewax.io/blog/child-routes-tabs-vue-3?utm_source=rss
---

Hello, I'm Konrad Sieńkowski and I am a front-end developer & UI designer here at Bytewax. I want to share with you something that I worked on recently. In this article, I'll walk through the steps to set up a new Vue application, configure the router for nested routes, create the AppTabs.vue component, and customize your tabs using route meta fields for labels and icons. By the end, you'll know how to make an easy yet flexible solution for displaying child routes in tabs. So, let's dive in!

_For those eager to dive in, check out the [project repository](https://github.com/konradsienkowski/vue-3-child-route-tabs/) on Github._

## Prerequisites

First of all, we're going to create a fresh, new application using `&gt; npm init vue@latest`. The `vue-create` tool is going to ask you about including optional features in the project. The only one required for that tutorial is **Vue Router**. I chose Typescript & Prettier as well, but it's up to your personal preferences.

## Preparing routes & structure

Once you follow the instructions on installing dependencies and running the app, you can start customizing the application. My first step was to simplify `app.vue` a bit:

```
&lt;template&gt;
  &lt;nav&gt;
    &lt;RouterLink to=&quot;/&quot;&gt;Home&lt;/RouterLink&gt;
    &lt;RouterLink to=&quot;/tabs&quot;&gt;Tabs demo&lt;/RouterLink&gt;
  &lt;/nav&gt;

  &lt;RouterView /&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import { RouterLink, RouterView } from &apos;vue-router&apos;
&lt;/script&gt;

```

Since we're focusing on nested/child routes in this article, there's no need to spend much time on the homepage. I've also renamed default `AboutView.vue` to `TabsView.vue` and created bunch of example views in `views/tabs`, called `TabsAbout.vue`, `TabsBlog.vue`, `TabsContact.vue`, `TabsRelated.vue`. We're going to include them in our routes structure in the next step.

```
- views
-- tabs
--- TabsAbout.vue
--- TabsBlog.vue
--- TabsContact.vue
--- TabsRelated.vue
-- HomeView.vue
-- TabsView.vue

```

As we have a simple structure for our views/pages, now it's time to include them in router configuration. Let's open `router/index.ts` now and adjust it to our needs:

```
import { createRouter, createWebHistory } from &apos;vue-router&apos;
import HomeView from &apos;../views/HomeView.vue&apos;

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: &apos;/&apos;,
      name: &apos;home&apos;,
      component: HomeView
    },
    {
      path: &apos;/tabs&apos;,
      name: &apos;tabs&apos;,
      component: () =&gt; import(&apos;../views/TabsView.vue&apos;),
      children: [
        {
          name: &apos;about&apos;,
          path: &apos;&apos;,
          component: () =&gt; import(&apos;../views/tabs/TabsAbout.vue&apos;),
        },
        {
          name: &apos;blog&apos;,
          path: &apos;blog&apos;,
          component: () =&gt; import(&apos;../views/tabs/TabsBlog.vue&apos;),
        },
        {
          name: &apos;contact&apos;,
          path: &apos;contact&apos;,
          component: () =&gt; import(&apos;../views/tabs/TabsContact.vue&apos;),
        },
        {
          name: &apos;related&apos;,
          path: &apos;related&apos;,
          component: () =&gt; import(&apos;../views/tabs/TabsRelated.vue&apos;),
        },
      ]
    }
  ]
})

export default router

```

Now, our application has nested/children routes which we can use to display tabs in the component.

## Tabs component

In this step, we're going to create our tab component, include it in the first-level route view and then extend it with additional features. First of all, we're going to create file called `AppTabs.vue` in `components` directory. Since our component is going to be flexible and might be used in different routes, we're following [Vue naming convention](https://v2.vuejs.org/v2/style-guide/?redirect=true#Base-component-names-strongly-recommended) for base components.

Let's start from the `&lt;script setup&gt;` section. We're using `useRouter()` composable there to access the router instance. Then, we're using it to define `tabs` computed property.

```
&lt;script setup lang=&quot;ts&quot;&gt;
import { computed, type ComputedRef } from &apos;vue&apos;
import { useRouter, RouterView, type RouteRecordRaw } from &apos;vue-router&apos;

// Use children routes for the tabs
const router = useRouter()
const tabs: ComputedRef&lt;RouteRecordRaw[] | undefined&gt; = computed(() =&gt; {
  const currentRoute = router.currentRoute.value.name
  return router.options.routes?.find(
    (route) =&gt;
      route.name === currentRoute || route.children?.find((child) =&gt; child.name === currentRoute)
  )?.children
})
&lt;/script&gt;

```

After getting the current route name using `router.currentRoute` property, we're using it to find it within the routes array (either within top-level routes and their children) and return its children routes. Now it's time to include it in the component template:

```
&lt;template&gt;
  &lt;div class=&quot;tabs&quot; v-if=&quot;tabs&quot;&gt;
    &lt;nav class=&quot;tabs__nav&quot;&gt;
      &lt;RouterLink
        v-for=&quot;tab in tabs&quot;
        :key=&quot;tab.name&quot;
        class=&quot;tabs__nav-item&quot;
        :to=&quot;{ name: tab.name }&quot;
      &gt;
        {{ tab.name }}
      &lt;/RouterLink&gt;
    &lt;/nav&gt;
    &lt;div class=&quot;tabs__wrapper&quot;&gt;
      &lt;RouterView v-slot=&quot;{ Component }&quot;&gt;
        &lt;Transition name=&quot;fade&quot; mode=&quot;out-in&quot;&gt;
          &lt;component :is=&quot;Component&quot;&gt;&lt;/component&gt;
        &lt;/Transition&gt;
      &lt;/RouterView&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

```

Inside the `&lt;div&gt;` wrapper, we have two parts of our component:

- navigation / tabs, where we iterate over output of `tabs` computed getter and display links of children routes,
- tabs wrapper, where we're using native `&lt;RouterView&gt;` and its v-slot api to [wrap nested route's content in `&lt;Transition&gt;` component](https://router.vuejs.org/guide/advanced/transitions.html#transitions).

Now we can include our component in the `TabsView.vue` code:

```
&lt;template&gt;
  &lt;div class=&quot;view&quot;&gt;
    &lt;AppTabs /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import AppTabs from &apos;@/components/AppTabs.vue&apos;
&lt;/script&gt;

```

And take a look at the result: ![Vite-App.gif](https://images.production.bytewax.io/Vite_App_2f6e016637.gif)

## Extending & styling up the tabs

Our tabs work nice, and we can easily include them in any view that has child routes. However, the tabs navigation uses `route.name` as a link label, and [route names](https://router.vuejs.org/guide/essentials/named-routes.html) should rather remain simple and easy to use. We can extend our solution with route props to include custom tab label & icon for each child route.

### Use custom route props

Before extending our component's code, let's add [meta field](https://router.vuejs.org/guide/advanced/meta.html) to each nested route in `router/index.ts`:

```
children: [
  {
    name: &apos;about&apos;,
    path: &apos;&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsAbout.vue&apos;),
    meta: { tabLabel: &apos;About&apos; }
  },
  {
    name: &apos;blog&apos;,
    path: &apos;blog&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsBlog.vue&apos;),
    meta: { tabLabel: &apos;Blog&apos; }
  },
  {
    name: &apos;contact&apos;,
    path: &apos;contact&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsContact.vue&apos;),
    meta: { tabLabel: &apos;Contact&apos; }
  },
  {
    name: &apos;related&apos;,
    path: &apos;related&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsRelated.vue&apos;),
    meta: { tabLabel: &apos;Related&apos; }
  },
]

```

Now, we can use `tabLabel` value in our `AppTabs.vue` component:

```
&lt;RouterLink
  v-for=&quot;tab in tabs&quot;
  :key=&quot;tab.name&quot;
  class=&quot;tabs__nav-item&quot;
  :to=&quot;{ name: tab.name }&quot;
&gt;
  &lt;span class=&quot;tabs__nav-label&quot; v-if=&quot;tab.meta?.tabLabel&quot;&gt;{{ tab.meta.tabLabel }}&lt;/span&gt;
&lt;/RouterLink&gt;

```

### Add material icons to tabs navigation

Our tabs navigation is going to look better with icons. Let's install Google's Material Symbols library using npm package: `npm install material-symbols@latest` and include it in `main.ts` (`main.js` if you're not using typescript):

```
import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;
import router from &apos;./router&apos;

import &apos;material-symbols/outlined.css&apos;;
import &apos;./assets/main.css&apos;

const app = createApp(App)

app.use(router)

app.mount(&apos;#app&apos;)

```

Then, we can add `tabIcon` properties to route meta fields, filling it with the icon codes:

```
children: [
  {
    name: &apos;about&apos;,
    path: &apos;&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsAbout.vue&apos;),
    meta: { tabLabel: &apos;About&apos;, tabIcon: &apos;group&apos; }
  },
  {
    name: &apos;blog&apos;,
    path: &apos;blog&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsBlog.vue&apos;),
    meta: { tabLabel: &apos;Blog&apos;, tabIcon: &apos;feed&apos; }
  },
  {
    name: &apos;contact&apos;,
    path: &apos;contact&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsContact.vue&apos;),
    meta: { tabLabel: &apos;Contact&apos;, tabIcon: &apos;email&apos; }
  },
  {
    name: &apos;related&apos;,
    path: &apos;related&apos;,
    component: () =&gt; import(&apos;../views/tabs/TabsRelated.vue&apos;),
    meta: { tabLabel: &apos;Related&apos;, tabIcon: &apos;star&apos; }
  },
]

```

After that, we're ready to include them in the component:

```
&lt;RouterLink
  v-for=&quot;tab in tabs&quot;
  :key=&quot;tab.name&quot;
  class=&quot;tabs__nav-item&quot;
  :to=&quot;{ name: tab.name }&quot;
&gt;
  &lt;span class=&quot;tabs__nav-icon material-symbols-outlined&quot; v-if=&quot;tab.meta?.tabIcon&quot;&gt;{{
    tab.meta.tabIcon
  }}&lt;/span&gt;
  &lt;span class=&quot;tabs__nav-label&quot; v-if=&quot;tab.meta?.tabLabel&quot;&gt;{{ tab.meta.tabLabel }}&lt;/span&gt;
&lt;/RouterLink&gt;

```

Done! We have custom icons & labels based on route meta fields displayed in our Tabs component. Now it's time to add final styling touch with CSS. ![icons.png](https://images.production.bytewax.io/icons_e127e03029.png)

## Styling up the component

You can style up the component on your own, customizing it fully to your needs or use code below including it in `AppTabs.vue` below:

```
&lt;style&gt;
.tabs {
  border: 1px solid rgba(0, 0, 0, 0.2);
  border-radius: 0.5rem;
}
.tabs__wrapper {
  padding: 1.5rem 2rem 2rem 2rem;
}
.tabs__nav {
  display: flex;
  flex-direction: row;
  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
.tabs__nav-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  text-decoration: none;
  padding: 1rem;
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
  color: rgba(0, 0, 0, 0.87);
  transition: border-color 0.25s ease-in-out;
}
.tabs__nav-icon {
  margin-right: 0.5rem;
  color: rgba(0, 0, 0, 0.38);
}
.tabs__nav-item:hover {
  border-color: #ccc;
}
.tabs__nav-item.router-link-exact-active {
  border-color: var(--green);
  font-weight: 600;
}
&lt;/style&gt;

```

_Note: Following [BEM naming convention](https://getbem.com/naming/) is easier using SCSS but I didn't want to fill the example with extra dependencies._

Our tab component looks pretty slick now: ![Vite-App-2.gif](https://images.production.bytewax.io/Vite_App_2_739c51d2d1.gif)

## Instead of conclusion

Now, I encourage you to give it a try, explore further customizations, and share your experiences and improvements with [our community](https://join.slack.com/t/bytewaxcommunity/shared_invite/zt-1lhq9bxbr-T3CXxR_9RIUGb4qcBK26Qw). Let's continue building more efficient and elegant applications together!