Flexible tabs in VueJS [Part 1]

Hello everyone. In this post I’m going to show you how to create a tabbed navigation in VueJS. Tabs are a very flexible and easy way to switch between content. Users are very accustomed with them because, well, they use them already. In their browsers! Working with a tabbed layout is even easier with Vue.

To speed things up and reduce the time spent styling the tabs we are going to use Bootstrap 4. At the time of this writing, Bootstrap 4 is still in alpha version but it’s very stable and brings in some powerful new features. So I’ve ditched Bootstrap 3 in favor of its newer edition. After adding Bootstrap 4 let’s create 3 simple tabs using the ‘nav-tabs’ Bootstrap class:

<div class="container-fluid">
    <div class="row">
        <div class="col-12">
            <ul class="nav nav-tabs">
                <li class="nav-item">
                    <a class="nav-link active" href="#">Tab 1</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Tab 2</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Tab 3</a>
                </li>
            </ul>
        </div>
    </div>
</div>

You can see the result we get is very satisfying, taking into account the amount of code we had to write to produce this.

tabblog1

Now that we’ve created the tabs we need a way to fill in their contents. The best tool for this job is a Bootstrap card

So let’s place the following code snippet right under our tabs:

<div class="card tab-contents">
    <div class="card-block">
        <div class="card-title">
            Tab Contents here
        </div>
    </div>
</div>

The result is almost perfect:tabblog2

We need to remove this ugly line under the active tab so that our design can be a little bit more fluent and consistent.

.card.tab-contents {
    border-top: none;
}

Much better now:

tabblog3

Bootstrap gives us a stylistic starting point. But I think we have to improve our tabs to give them a more unique look and feel. To do that I changed some things in the HTML and I’ve added the following Sass styles and a font awesome icon on each tab that will serve as a ‘close button’. I’ve also made each tab a flex container, so that everything is aligned properly within it. A flex container will also be useful to us later when we will add a text box for renaming the tab

HTML:

<div class="container-fluid">
    <div class="row">
        <div class="col-12">
            <ul class="nav nav-tabs">
                <li class="nav-item">
                    <a class="nav-link d-flex align-items-center active tab" href="#">
                        <span>Tab 1</span>
                        <button class="icon-btn">
                            <i class="fa fa-times" aria-hidden="true"></i>
                        </button>
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link d-flex align-items-center tab" href="#">
                        <span>Tab 2</span>
                        <button class="icon-btn">
                            <i class="fa fa-times" aria-hidden="true"></i>    
                        </button>
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link d-flex align-items-center tab" href="#">
                        <span>Tab 3</span>
                        <button class="icon-btn">
                            <i class="fa fa-times" aria-hidden="true"></i>
                        </button>
                    </a>
                </li>
            </ul>
            <div class="card tab-contents">
                <div class="card-block">
                    <div class="card-title">
                        Tab Contents here
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Sass:

$color-transition-duration: 0.8s;
$accent-color: #2980b9;
$x-hover-color: #c0392b;
$smaller-nav-item-padding: 8px;
$icon-size: 0.875rem;

ul.nav-tabs {
    margin-top: 12px;
}

.card.tab-contents {
    border-top: none;
    border-radius: 0;
} 

.nav-link.tab {
    border-radius: 0;
    
    //Override the 16px Bootstrap default to give it a more tab-like feel
    padding-right: $smaller-nav-item-padding;
    
    span {
        transition: color $color-transition-duration;    
        color: black;
        opacity: 0.54;
        &:hover {
            color: $accent-color;
        }
    }
    
    &.active {
        span {
            opacity: 1;
        }
    }
           
    .icon-btn {
        margin-left: 6px;
        text-decoration: none;    
        background-color: transparent;
        border: none;
        cursor: pointer;
        outline: none;
        font-size: $icon-size;

        .fa-times {
            opacity: 0.54;
            transition: color $color-transition-duration;
            
            &:hover {
                color: $x-hover-color;
            }
        }    
    }    
}

I’ve added some top margin, made the borders rectangular, grayed out the inactive tabs and added a nice transition. Nothing fancy, with enough room for further customization.

One last thing we are going to need is an additional button that will open up a new tab

<li class="nav-item">
    <a class="nav-link d-flex align-items-center tab add-btn" href="#">
        <button class="icon-btn">
            <i class="fa fa-plus" aria-hidden="true"></i>
        </button>
    </a>
</li>

And its styles:

.nav-link.tab {
	&.add-btn {
        padding-left: $smaller-nav-item-padding;        
        
        .icon-btn {
            color: $accent-color;
            margin: 0;    
        }
    }
}

Now that we’ve built the HTML/CSS part we need to move to the actual logic that will allow our tabs to work.

We will create a VueJS view model, add an array of objects where each objects represents a tab and we will dynamically load the tabs and their content on screen. We will also need an activeTab object to store the selected tab which by default will be the first one. All these requirements are expressed in the following view model:

JavaScript:

let app = new Vue({
	el: '#app',
    data: {
    	activeTab: null,
    	tabs: [
        	{
            	id: 1,
            	title: 'Tab 1',
                content: {
                	header: 'Tab 1 Header',
                    content: 'Tab 1 Content: Lorem ipsum dolor sit amet, consectetur adipiscing elit'
                }
            },
            {
            	id: 2,
            	title: 'Tab 2',
                content: {
                	header: 'Tab 2 Header',
                    content: 'Tab 2 Content: Praesent feugiat aliquam odio, at dictum nibh. Ut vitae quam nec nunc rhoncus sodales. In luctus venenatis auctor'
                }
            },
            {
            	id: 3,
            	title: 'Tab 3',
                content: {
                	header: 'Tab 3 Header',
                    content: 'Tab 3 Content:  Praesent consectetur luctus tortor vel feugiat. Vestibulum vitae tempor ipsum, quis pharetra augue. '
                }
            }
        ]
    },
    created: function() {
    	this.activeTab = this.tabs[0];
    }
})

Now that we can dynamically load our tabs, the view greatly simplifies:

HTML:

<div id="app" class="container-fluid" v-cloak>
    <div class="row">
        <div class="col-12">
            <ul class="nav nav-tabs">
                <li v-for="tab of tabs" class="nav-item">
                    <a v-bind:class="{'nav-link d-flex align-items-center tab': true, 'active': (tab.id == activeTab.id) }" href="#">
                        <span>{{tab.title}}</span>
                        <button class="icon-btn">
                            <i class="fa fa-times" aria-hidden="true"></i>
                        </button>
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link d-flex align-items-center tab add-btn" href="#">
                        <button class="icon-btn">
                            <i class="fa fa-plus" aria-hidden="true"></i>
                        </button>
                    </a>
                </li>
            </ul>
            <div class="card tab-contents">
                <div class="card-block">
                    <div class="card-title">
                        {{activeTab.content.header}}
                    </div>
                    <div class="card-text">
                        {{activeTab.content.content}}
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

As you can see we have a v-for loop that populates a template of an li with all the information about the tab. Check the created() method, which is a Vue lifecycle hook called immediately after the view model is initialized. There we make activeTab point, by default, to the first tab

Next, we want a way to cycle through tabs, select one and view its contents. So basically ,we need to change the activeTab whenever we click on a tab and Vue will take care of the rest. To do that we create an ‘activateTab’ in our methods object with just one line of code:

activateTab: function(tab) {
       this.activeTab = tab;
}

Now we only need to handle the event click in the view and pass the tab object to activateTab like this:

 v-on:click="activateTab(tab)"

So our list item template now looks like this:

<li v-for="tab of tabs" class="nav-item">
    <a v-bind:class="{'nav-link d-flex align-items-center tab': true, 'active': (tab.id == activeTab.id) }" href="#" v-on:click="activateTab(tab)">
        <span>{{tab.title}}</span>
        <button class="icon-btn">
            <i class="fa fa-times" aria-hidden="true"></i>
        </button>
    </a>
</li>

You can check the code from the first part of this tutorial in the following fiddle:

In the next part of this tutorial, I’m going to show you how to add, edit and delete tabs. Stay tuned!