Jim Phillips

Twitter Bootstrap has recently shipped an alpha release for version 4. There has been much written on the new and updated features since the August release. I decided to get my hands dirty and convert an existing application from version 3 to version 4. As the release moves to beta and production, I will update this article. My experience, hopefully, will make the process easier for others looking to do the same.

The application I am converting uses AngularJS for the front end. The js and css files are managed using gulp to dynamically build index.html with minified css and js files.

Installation and Setup

First things first, let's get the code. One option is to just use the compiled CSS and js. This is fine for the CSS if you just want to use the default styles. However, I want to customize the look and feel with our own branding, so I downloaded the source files. For the JavaScript, I just use the compiled js in the dist directory.

This is where I ran into my first problem; how to customize the CSS. In the source file, there is a _variables.scss file. It would be easy enough to just edit the Bootstrap source file and compile the CSS with each build. There are two problems with this approach:

  1. I have a hard rule of never editing vendor code.
  2. Bootstrap will inevitably upgrade its versions.

    We use Bower to manage client-side vendor code. Editing a copied version of Bootstrap would mean manual updates with any version change. That's no fun. Also, we would be checking vendor code into our repository. That is just wasteful and potentially confusing.

Instead, I utilize gulp to build the CSS for us:

gulp.task("build:bootstrap", function () {
  console.log("plugins: ", plugins);
  return gulp.src(['client/scss/custom-bootstrap.scss'])
    .pipe(plugins.sass())
    .pipe(plugins.autoprefixer())
    .pipe(gulp.dest('client/css')); // compile to a temporary file
});

gulp.task("build:main-css", function () {
  console.log("plugins: ", plugins);
  return gulp.src(['client/scss/main.scss'])
    .pipe(plugins.sass())
    .pipe(plugins.autoprefixer())
    .pipe(gulp.dest('client/css')); // compile to a temporary file
});

gulp.task('build:create-css', function () {
  return gulp.src(config.files.client_all_css) // control the import order
    .pipe(plugins.minifyCss())
    .pipe(plugins.concat('app.min.css'))
    .pipe(gulp.dest('client/dist/css'));
});

gulp.task('build:css', gulp.series(
  'build:bootstrap',
  'build:main-css',
  'build:create-css'
));

You will notice I have separated out custom-bootstrap.scss and main.scss. This is because the application also uses Angular plugins that depend on Bootstrap. I want to customize some of the CSS, both in Bootstrap and other vendors' CSS. In order to build the CSS in the proper order, the compiled Bootstrap (with our variables definitions) must be separate. The order of CSS in the minified file is:

  1. bootstrap.css
  2. other-vendors.css
  3. main.css (overriding bootstrap and other-vendors css)

"Hang on," you say. "I thought you could just set the variables to customize Bootstrap." At least that's what I thought when I started, but it turned out not to be the case.

Customizing the Look and Feel

The first thing I change is the primary brand color. This is easy enough. It is the $brand-primary variable. Bootstrap uses a blue. We use a green. To allow easy customization of the major variables, Bootstrap uses $[variable-name]: [value] !default; the !default is what allows for overriding the variables. Unfortunately, this is not used throughout the SCSS code. For example, in the new card component, the card-header background color uses a variable that is not overridable. Instead, you have to override the styling traditionally. I want to use the primary brand color for the card-header background. In order to do this, I created 3 SCSS files: _variables.scss, custom-bootstrap.scss, and main.scss. I separated out _variablesin order to include it in the other two:

  1. _variables.scss:

    $enable-flex: true;
    $enable-rounded: false;
    $brand-primary: #00853f;
    
  2. custom-bootstrap.scss:

    @import '_variables';
    @import '../vendor/bootstrap4/scss/bootstrap';
    

    Note that the local _variables import must be before the Bootstrap import.

  3. main.scss:

    @import '_variables';
    @import '../vendor/bootstrap4/scss/_variables';
    
    body {
      // override default background-color with an image
      background-image: url("../img/content-bg.png");
    }
    
    .card-header {
      background-color: $brand-primary;
    }
    
    .card-block {
      // use white background for card contents
      background-color: #fff;
    }
    

    Remember to include both your local _variables and bootstrap _variables for access to all.

Perhaps this will change with the beta or prod releases. But even if all variables could be overridden in a local _variables file, there will be other components you will want to customize. This is a clean way to incorporate all variables in your custom main.scss file, allowing you to extend Bootstrap.

The Nitty Gritty

This is where I get into the HTML changes that become necessary with the conversion. The first thing I did once I got Bootstrap installed and set up was to see how it looked with the existing HTML. Some elements were laid out wrong, others just did not display. First, I will go through some classes that are missing. Next, I will explain the edits necessary for some of the major components that changed. This is far from an exhaustive list, but it should hit on some commonly used components and give you an idea of what to look for.

What's missing?

In some instances, Bootstrap decided to just drop the class altogether. The following sections should give you guidance for a global search and replace strategy. For each item, I suggest starting with a single instance to get the exact changes you want, and then doing the search and replace.

Panels, wells, and thumbnails

I used the card component as an example of something that needed standard CSS overriding. It is also a new feature that replaces panels, wells, and thumbnails from Bootstrap 3. Our app uses panels extensively, meaning a rewrite of the HTML. This is simple enough.

<div class="panel panel-default">
  <div class="panel-heading">
    ...
  </div>
  <div class="panel-body">
    ...
  </div>
</div>

becomes

<div class="card card-inverse">
  <div class="panel-heading">
    ...
  </div>
  <div class="card-block">
    ...
  </div>
</div>

Note: I use .card-inverse because of the earlier customization that uses our dark $brand-primary background-color. This class will display a white font.

.navbar-inverse

This may make a comeback. The documentation mentions it and other sections have an -inverse, but it is not in the CSS as of the writing of this article.

<nav class="navbar navbar-inverse">

becomes

<nav class="navbar navbar-dark bg-inverse">

.form-horizontal

When I started the app without any edits the first thing I noticed on the login page was that the form was all jacked up. .form-horizontal was a simple way to lay out forms with a label on the left and the form field on the right within a .form-group. It removed the need to add .row to the form-group div. Luckily, this is an easy fix. Also, it is probably a more clear way to lay out the form. In retrospect, I probably should have used .row from the start.

<form class="form-horizontal" ng-submit="loginCtrl.authenticate()">
  <div class="form-group">
    <div class="col-md-2 col-md-offset-3">
      <label class="control-label" for="email">Email:</label>
    </div>
    <div class="col-md-2">
      <input type="email" id="email" ng-model="loginCtrl.email" ng-required="true">
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-2 col-md-offset-3">
      <label class="control-label" for="digitalPassword">Password:</label>
    </div>
    <div class="col-md-2">
      <input type="password" id="digitalPassword" ng-model="loginCtrl.digitalPassword" ng-required="true">
    </div>
  </div>
  <div class="form-group">
    <div class="col-md-2 col-md-offset-5">
      <button class="btn btn-primary" type="submit">Login</button>
    </div>
  </div>
</form>

becomes

<form ng-submit="loginCtrl.authenticate()">
  <div class="form-group row">
    <div class="col-md-2 col-md-offset-3">
      <label class="control-label" for="email">Email:</label>
    </div>
    <div class="col-md-2">
      <input type="email" id="email" ng-model="loginCtrl.email" ng-required="true">
    </div>
  </div>
  <div class="form-group row">
    <div class="col-md-2 col-md-offset-3">
      <label class="control-label" for="digitalPassword">Password:</label>
    </div>
    <div class="col-md-2">
      <input type="password" id="digitalPassword" ng-model="loginCtrl.digitalPassword" ng-required="true">
    </div>
  </div>
  <div class="form-group row">
    <div class="col-md-2 col-md-offset-5">
      <button class="btn btn-primary" type="submit">Login</button>
    </div>
  </div>
</form>

.page-header

Removing this speaks to a larger philosophy with Bootstrap v4 of avoiding the use of margin-top. This is discussed in their reboot documentation. Basically, they say to avoid it because it may yield unexpected results. I can live with that. Goodbye .page-header. Goodbye .margin-top.

.btn-default

This was a gray background that we used for our 'cancel' buttons, with the 'save' using .btn-primary. Simply replace btn-default with btn-secondary. See also the other button classes for more options.

Glyphicons

Gone. No replacement. This was perhaps the most disheartening change. I personally loved the glyphicons. Alas, I will live. There are several options, but all require rewriting the HTML to use the new implementation. The most similar that I found was Font Awesome. They are still font-based, and the implementation is practically identical. A simple Bower install with an update to the gulp build includes font-awesome. The rest is just a matter of finding a comparable image, doing a global search, and replace.

Component changes

Some components changed dramatically, at least in terms of the HTML used to render them. The changes were made with the intent to simplify the usage and allow for more flexibility on what element to use.

Navbars

Our application uses a common Navbar for accessing the various sections. Once I logged in, the Navbar did not show. Oops! I can't use the app. This is because in v3, the Navbar forced you to use a ul. With v4, this is no longer the case. That is a nice feature allowing flexibility. It also means rewriting HTML. Since there is no assumed field, you must explicitly set the nav-item.

<nav class="navbar navbar-inverse">
  <div class="container">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <a class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse" title="Toggle navigation">
        <span class="sr-only">Toggle navigation</span>
        <div class="icon-bar"></div>
        <div class="icon-bar"></div>
        <div class="icon-bar"></div>
      </a>
      <a href="#" ui-sref="loggedIn.welcome"><img class="navbar-brand" src="img/logo.png"/></a>
    </div>

    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="navbar-collapse collapse"  id="navbar-collapse-1">
      <ul class="nav navbar-nav">
        <li ng-show="hasAccess('admin')" ui-sref-active="active">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            User Management
            <span class="caret"></span>
          </a>
          <ul class="dropdown-menu">
            <li><a href="#" ui-sref="loggedIn.userManagement.listUsers">View Users</a></li>
            <li><a href="#" ui-sref="loggedIn.userManagement.createUser">Create New User</a></li>
            <li><a href="#" ui-sref="loggedIn.userManagement.manageRoles">Manage Roles</a></li>
          </ul>
        </li>
        <li ng-show="hasAccess('eventAdmin')" ui-sref-active="active">
          <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            Outreach Events
            <span class="caret"></span>
          </a>
          <ul class="dropdown-menu">
            <li><a href="#" ui-sref="loggedIn.outreachEvents.listOutreachEvents">View Outreach Events</a></li>
            <li><a href="#" ui-sref="loggedIn.outreachEvents.createOutreachEvent">Create New Outreach Event</a></li>
          </ul>
        </li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
        <li>
          <a href="#" ng-click="loginCtrl.logout()"><span class="glyphicon glyphicon-off"></span><span>&nbsp;&nbsp;Logout</span></a></li>
      </ul>
    </div><!-- /.navbar-collapse -->
  </div><!-- /.container-->
</nav>

becomes

<nav class="navbar navbar-dark bg-inverse">

  <!-- Brand and toggle get grouped for better mobile display -->
  <div class="navbar-brand" ui-sref="loggedIn.welcome"><img class="img-responsive" src="img/logo.png"/></a>
    <button class="navbar-toggler hidden-sm-up" data-toggle="collapse" data-target="#navbar-collapse-1" title="Toggle navigation">
      &#9776;
    </button>
  </div>

  <!-- Collect the nav links, forms, and other content for toggling -->
  <div class="collapse navbar-toggleable-xs"  id="navbar-collapse-1">
    <div class="nav navbar-nav">
      <div class="nav-item dropdown" ng-show="hasAccess('admin')" ui-sref-active="active">
        <div id="user-dropdown" class="dropdown-toggle" data-toggle="dropdown" type="button" aria-haspopup="true" aria-expanded="false">
          User Management
          <span class="caret"></span>
        </div>
        <div class="dropdown-menu" aria-labelledby="user-dropdown">
          <div class="dropdown-item" ui-sref="loggedIn.userManagement.listUsers">View Users</div>
          <div class="dropdown-item" href="javascript:void()" ui-sref="loggedIn.userManagement.createUser">Create New User</div>
          <div class="dropdown-item" href="javascript:void()" ui-sref="loggedIn.userManagement.manageRoles">Manage Roles</div>
        </div>
      </div>
      <div class="nav-item dropdown" ng-show="hasAccess('eventAdmin')" ui-sref-active="active">
        <div class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
          Outreach Events
          <span class="caret"></span>
        </div>
        <div class="dropdown-menu">
          <div class="dropdown-item" ui-sref="loggedIn.outreachEvents.listOutreachEvents">View Outreach Events</div>
          <div class="dropdown-item" ui-sref="loggedIn.outreachEvents.createOutreachEvent">Create New Outreach Event</div>
        </div>
      </div>
      <div class="nav-item pull-right">
        <div ng-click="loginCtrl.logout()"><span class="glyphicon glyphicon-off"></span><span>&nbsp;&nbsp;Logout</span></div>
      </div>
    </div>
  </div><!-- /.navbar-collapse -->
</nav>

.dropdown

You will notice in the previous example that the dropdowns also changed. The difference follows the same pattern as the Navbar. You have to explicitly set the .dropdown-item within the dropdown menu.

Third-party Dependencies

Finally, other vendors provide plugins that rely on Bootstrap. Since the current version of Bootstrap v4 is in alpha, it is unlikely the vendors will convert their code any time soon. The current versions of some plugins use v3. This provides a dilemma as an early adopter of v4. For the plugins that use features that are now gone (glyphicons) or dramatically changed (dropdowns), what to do? Do we just hope the third-parties will update their code in time? Do we write our own components to duplicate the functionality? Or should we just find a non-Bootstrap alternative? Most likely, you are using the plugin because it uses Bootstrap and matches the look and feel of the rest of the application. Ultimately, you must decide what is best for your use case. Personally, I love coding, so creating the functionality appeals to me. However, my manager may have a problem with me spending time on writing something that already exists. Most likely I will try to find comparable plugins that do not conflict with the Bootstrap 4 changes.