Build Chrome-like Tabs with Bootstrap and AngularJS

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

If you develop web applications you’ve probably used tabs at some point in your app to improve the layout and make it more organized and user friendly. Luckily today there are several libraries like Bootstrap and jQuery UI that do the job perfectly, so we don’t need to code the tabs from scratch.

For this tutorial we’ll try to make the tabs not just to look like the one in Chrome browser but also maintain some of the functionality like dynamically shrinking and growing depending on the number of opened tabs. We’ll use Bootstrap with AngularJS because they play well together, are very simple to use and are perfect fit for this tutorial.

The final product will have this properties:

  • Opening and closing of tabs
  • Dynamic tabs width depending of the number of opened tabs
  • Moving highlight for the inactive tabs when the mouse pointer is over them
  • Chrome look & feel

What these tabs won’t support is reordering and complex animations when opening and closing the tabs.

The tabs will look like these: Chrome-like tabs in action

The tutorial is updated to use angular directive for the tabs moving highlight, and removed the duplicated bootstrap reference. Thanks Ronnie Gardocki and Jesus Rodriguez for the tips!

To make it digestible we’ll split the process of making the tabs in 5 sections:

  1. Prepare project structure
  2. Automate it with Gulp
  3. Coding the HTML
  4. Coding the AngularJS module
  5. Coding the CSS with Compass (Sass)

This tutorial suppose you already have basic knowledge in NodeJS, AngularJS and Sass. Even the less experienced should be able to finish this tutorial by following the steps below.

Prerequisites

In order to build this project you need to have Node.js and npm (Node Package Manager) installed on your system. To compile the Sass source to CSS, you’ll need Ruby, Sass and Compass. Installing them is beyond the scope of this tutorial and will assume you already have them installed. In case you need any help you can always search the web, there’re plenty of resources on the subject.

1. Prepare the project

Although this is small piece of work we’d like to keep things neat and clean that’s why we’ll use NodeJS for development as it fits perfectly for this kind of project. Let’s start with the application folder structure:

dist/              // holds the final files for distribution
|  js/             // concatenated (and minified) javascript
|  css/            // compiled css
source/            // holds all source files
|  javascript/     // javascript source
|  |  app.js
|  sass/           // sass source
|  |  style.scss
|  index.html
package.json
gulpfile.js

And the package.json file should look like this:

{
  "name": "chrome-tabs",
  "version": "1.0.0",
  "description": "Web chrome-like tabs made with Bootstrap and AngularJS",
  "engines": {
    "node": ">= 0.10"
  },
  "devDependencies": {
    "gulp": "^3.8.11",
    "gulp-compass": "^2.0.3",
    "gulp-concat": "^2.5.2",
    "gulp-jshint": "^1.9.2",
    "gulp-sourcemaps": "^1.3.0",
    "gulp-uglify": "^1.1.0",
    "gulp-util": "^3.0.4",
    "jshint-stylish": "^1.0.1"
  }
}

Make sure before you start developing you’ve installed all required dependencies from command line with npm install.

2. Automate everything

To automate tasks like compiling Sass sources we’ll use Gulp. If you haven’t tried it yet here’s great intro on how to set it up.

Before we start with writing the gulpfile.js file, let’s make sure we have Gulp installed globally with npm install --global gulp.

Next let’s go with including all modules and define the input and output files:

/* gulpfile.js */
var gulp  = require('gulp'),
    gutil = require('gulp-util'),

    jshint     = require('gulp-jshint'),
    compass    = require('gulp-compass'),
    concat     = require('gulp-concat'),
    uglify     = require('gulp-uglify'),
    sourcemaps = require('gulp-sourcemaps'),

    input  = {
      'html': 'source/*.html',
      'sass': 'source/sass/**/*.scss',
      'javascript': 'source/javascript/**/*.js'
    },

    output = {
      'html': 'dist',
      'stylesheets': 'dist/css',
      'javascript': 'dist/js'
    };

Than we’ll define the JavaScript error checking and concatenating/minifying tasks:

/* run javascript through jshint */
gulp.task('jshint', function() {
  return gulp.src(input.javascript)
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

/* concat javascript files, minify if --type production */
gulp.task('build-js', function() {
  return gulp.src(input.javascript)
    .pipe(sourcemaps.init())
      .pipe(concat('app.js'))
      //only uglify if gulp is ran with '--type production'
      .pipe(gutil.env.type === 'production' ? uglify() : gutil.noop())
    .pipe(sourcemaps.write())
    .pipe(gulp.dest(output.javascript));
});

The Sass build task to compile our styles:

/* compile scss files */
gulp.task('build-css', function() {
  return gulp.src(input.sass)
    .pipe(sourcemaps.init())
      .pipe(compass({
        css: 'dist/css',
        sass: 'source/sass'
        }))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest(output.stylesheets));
});

The HTML copy task:

/* copy any html files to dist */
gulp.task('copy-html', function() {
  return gulp.src(input.html)
    .pipe(gulp.dest(output.html));
});

The watch task that will automatically start the appropriate task when some of the files are changed:

/* Watch these files for changes and run the task on update */
gulp.task('watch', function() {
  gulp.watch(input.javascript, ['jshint', 'build-js']);
  gulp.watch(input.sass, ['build-css']);
  gulp.watch(input.html, ['copy-html']);
});

And finally the default Gulp task:

/* run the watch task when gulp is called without arguments */
gulp.task('default', ['jshint', 'build-js', 'build-css', 'copy-html', 'watch']);

To start the project we only need to run gulp on the command line. This will also watch for file changes thus making the project easier for development.

3. Set up the HTML

The HTML file is quite simple because it will contains only the tabs directive and includes the required dependencies for jQuery, AngularJS, and Bootstrap. We’ll begin with the skeleton of the HTML:

<!-- file: source/index.html -->
<!DOCTYPE html>
<!-- tell angular this is chromeTabsApp -->
<html lang="en" ng-app="chromeTabsApp">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Chrome tabs with Bootstrap and AngularJS</title>
    <!-- include the Bootstrap CSS -->
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
    <!-- include our compiled CSS -->
    <link href="css/style.css" rel="stylesheet">
  </head>
  <!-- this will be the only controller AppCtrl -->
  <body ng-controller="AppCtrl">

    <!-- PUT THE TABS DIRECTIVE HERE -->

    <!-- include the JavaScript dependencies: jQuery, AngularJS, AngularUI Bootstrap -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.1/ui-bootstrap-tpls.min.js"></script>
    <!-- include our JavaScript -->
    <script src="js/app.js"></script>
  </body>
</html>

If you’re wondering why we’re using jQuery in Angular app (which many consider it as bad practice) it’s because we need more advanced DOM events for our moving highlight to work.

The tabs directive looks like this:

<!-- set our tabs with the AngularJS Bootstrap directive -->
<tabset class="tab-container">
  <tab ng-repeat="tab in tabs" active="tab.active" tab-highlight> <!-- the tab highlight directive -->
    <tab-heading>
      <span>{{tab.title}}</span> <i class="glyphicon glyphicon-remove" ng-click="removeTab($event, $index)"></i> <!-- the tab close button -->
    </tab-heading>
    <h1>{{tab.content}}</h1>
  </tab>
  <!-- this is the open new tab button -->
  <button class="btn btn-default" ng-click="addTab()"></button>
</tabset>

You can see that we iterate over collection of tabs which drastically simplifies our code. Also for each tab we include close button and open new tab to the right of the tabs. Next let’s set the angular module.

4. Make it dynamic with AngularJS

To manage the tabs we’ll use simple array of objects in our angular controller. Here we’ll also make our moving highlight directive and initialize the tabs to use it. Let’s begin with declaring our module and controller with the tab highlight directive:

/* file: source/javascript/app.js */
// Declare the chromeTabsApp module and its dependency 'ui.bootstrap'
var app = angular.module('chromeTabsApp', ['ui.bootstrap']);
// Declare the AppCtrl controller
app
  .controller('AppCtrl', ['$scope', function ($scope) {
    
    // PLACE THE TABS CODE HERE

  }])
  .directive('tabHighlight', [function () {
    
    // PLACE THE MOVING HIGHLIGHT CODE HERE

  }]);

After this we’ll add our counter and tabs array:

// Tab counter
var counter = 1;
// Array to store the tabs
$scope.tabs = [];

Next we’ll add the functions for adding and removing tabs:

// Add tab to the end of the array
var addTab = function () {
  $scope.tabs.push({ title: 'Tab ' + counter, content: 'Tab ' + counter });
  counter++;
  $scope.tabs[$scope.tabs.length - 1].active = true;
};

// Remove tab by index
var removeTab = function (event, index) {
  event.preventDefault();
  event.stopPropagation();
  $scope.tabs.splice(index, 1);
};

// Initialize the scope functions
$scope.addTab    = addTab;
$scope.removeTab = removeTab;

At the end of our controller we’ll just add 10 tabs for demonstration purposes:

// For demonstration add 10 tabs
for (var i = 0; i < 10; i++) {
  addTab();
}

Next we’ll add the moving highlight functionality. For this we’ll use angular directive as this is best practice to use jQuery inside AngularJS applications.

return {
  restrict: 'A',
  link: function (scope, element) {
    // Here is the major jQuery usage where we add the event
    // listeners mousemove and mouseout on the tabs to initalize
    // the moving highlight for the inactive tabs
    var x, y, initial_background = '#c3d5e6';

    element
      .removeAttr('style')
      .mousemove(function (e) {
        // Add highlight effect on inactive tabs
        if(!element.hasClass('active'))
        {
          x = e.pageX - this.offsetLeft;
          y = e.pageY - this.offsetTop;

          // Set the background when mouse moves over inactive tabs
          element
            .css({ background: '-moz-radial-gradient(circle at ' + x + 'px ' + y + 'px, rgba(255,255,255,0.4) 0px, rgba(255,255,255,0.0) 45px), ' + initial_background })
            .css({ background: '-webkit-radial-gradient(circle at ' + x + 'px ' + y + 'px, rgba(255,255,255,0.4) 0px, rgba(255,255,255,0.0) 45px), ' + initial_background })
            .css({ background: 'radial-gradient(circle at ' + x + 'px ' + y + 'px, rgba(255,255,255,0.4) 0px, rgba(255,255,255,0.0) 45px), ' + initial_background });
        }
      })
      .mouseout(function () {
        // Return the inital background color of the tab
        element.removeAttr('style');
      });
  }
};

Although the code looks complex, it’s actually quite simple and it just replaces the tab background with radial gradient that starts at the current mouse pointer coordinates when placed over the tab.

With finished angular module the next thing to do is to style the tabs.

5. Styling the tabs

To make sure the tabs look like the one in Chrome browser we’ll have to override the default Bootstrap styles. This is not a straightforward task, so the best solution is to iterate several times through the design until we bring it closer to one similar to Chrome. For styling we’ll be using Sass and Compass which includes collections of Sass mixins and functions that will help us in making the code cleaner. There’s no simple way to separate the code into sections so below is the whole style for reference.

// file: source/sass/style.scss
@import "compass";

// The main tab container
.tab-container {
  background: #8dc8fb; // Make the background blue
  margin: 0;
  padding: 0;
  max-height: 40px;

  ul {
    &.nav-tabs {
      margin: 0;
      list-style-type: none;
      line-height: 40px;
      max-height: 40px;
      overflow: hidden;
      display: inline-block;
      @include display-flex; // This is the magic that will dynamically expand/shrink the width tabs
      padding-right: 20px;
      border-bottom: 5px solid #f7f7f7;

      $color: #c3d5e6; // Color for the disabled files

      // Inactive tab styles
      > li {
        $raduis: 28px 145px; // Radius to make the tabs look like trapezoid

        // Apply the radius
        @include border-top-left-radius($raduis);
        @include border-top-right-radius($raduis);

        margin: 5px -14px 0;
        padding: 0 30px 0 25px;
        height: 170px;
        background: $color;
        position: relative;
        width: 200px;
        max-width: 200px;
        min-width: 20px;
        border:1px solid #aaa;

        @include box-shadow(0 4px 6px rgba(0,0,0,.5));

        &:first-child {
          margin-left: 0;
        }

        &:last-of-type {
          margin-right: 0;
        }

        > a {
          display: block;
          max-width:100%;
          text-decoration: none;
          color: #222;
          padding: 3px 7px;

          span {
            overflow: hidden;
            white-space: nowrap;
            display: block;
          }

          &:focus,
          &:hover {
            background-color: transparent;
            border-color: transparent;
          }

          // The remove button styles
          .glyphicon-remove {
            color: #777;
            display: inline-block;
            padding:3px;
            font-size: 10px;
            position:absolute;
            z-index: 10;
            top:7px;
            right: -10px;
            @include border-radius(50%);

            &:hover {
              background: #d39ea3;
              color: white;
              @include box-shadow(inset 0 1px 1px rgba(0,0,0,.25));
              @include text-shadow(0 1px 1px rgba(0,0,0,.25));
            }
          }
        }

        // Active tab style
        &.active {
          z-index: 2;
          @include background-image(linear-gradient(white, #f7f7f7 30px));

          > a {
            background-color: transparent;
            border-color: transparent;
            border-bottom-color: transparent;

            &:focus,
            &:hover {
              background-color: transparent;
              border-color: transparent;
              border-bottom-color: transparent;
            }
          }
        }
      }

      // The open new tab button
      .btn {
        float: left;
        height: 20px;
        width: 35px;
        min-width: 35px;
        max-width: 35px;
        margin: 10px 0 0 0;
        border-color: #71a0c9;
        outline: none;

        @include transform(skew(30deg));

        &.btn-default {
          background: $color;

          &:hover {
            background: #d2deeb;
          }

          &:active {
            background: #9cb5cc;
          }
        }
      }
    }
  }

  // Styling the tab containter
  .tab-pane {
    padding: 60px 40px;
    text-align: center;

    &.active {
      border-top:1px solid #ddd;
    }
  }
}

You can notice that using the CSS3 display flex for the tabs container will make the individual tabs dynamically expand and shrink depending on the number of opened tabs. You can read complete guide for the Flexbox Layout here.

If you followed the steps above you should be able to build everything with running gulp from the command line. To see the fruits of your labor open dist/index.html file in your browser.

Live demo

Here’s a live Codepen demo where you can test and play a bit with the tabs.

See the Pen Chrome Tabs by Dimitar (@justd) on CodePen.

Dimitar Stojanov

Dimitar is regular guy passionate about helping others, that's why he built Invoicebus, a service for designers and developers where they can create and track their invoices and payments in a beautiful and simple manner.