How to make a Bootstrap 5 theme: typography

Now that our basic setup is done, we can start to customise the typography of our theme.

The scope of this tutorial is to start using the sass variables in scss/_variables.scss so we can change the look and feel of our typography while expanding our current setup.

The general idea

Import a custom font, use a modular scale, and extend Bootstrap 5 using scss/_variables.scss to customise displays, headings and inline text elements.

In the meanwhile, we’ll have a small webserver running with some live reload and an index.html to see how our typography is going on.

Extending the current setup: index.html and webserver

Let’s start creating our index.html with some markup. We’ll import css/styles.css as well, which you should have from the end of the last tutorial.

touch index.html

And now, we should have:

bootstrap5-theme
├── node_modules
├── .nvmrc
├── .gitignore
├── package-lock.json
├── package.json
├── css
│   └── styles.css
├── scss
│   ├── _variables.scss
│   └── styles.scss   
└── index.html

Let’s edit index.html and add the following:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="css/styles.css">
    <title>Bootstrap 5 Theme</title>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-12 col-lg-8 col-xl-6 mx-auto py-4">
                <h1>Displays</h1>
                <div class="border p-3 mb-4">
                    <h1 class="display-1">Display 1</h1>
                    <h1 class="display-2">Display 2</h1>
                    <h1 class="display-3">Display 3</h1>
                    <h1 class="display-4">Display 4</h1>
                    <h1 class="display-5">Display 5</h1>
                    <h1 class="display-6">Display 6</h1>
                </div>

                <h1>Headings</h1>
                <div class="border p-3 mb-4">
                    <h1>h1. Bootstrap heading</h1>
                    <h2>h2. Bootstrap heading</h2>
                    <h3>h3. Bootstrap heading</h3>
                    <h4>h4. Bootstrap heading</h4>
                    <h5>h5. Bootstrap heading</h5>
                    <h6>h6. Bootstrap heading</h6>
                </div>

                <h1>Inline text elements</h1>
                <div class="border p-3 mb-4">
                    <p class="lead">lead text here</p>
                    <p>basic text here</p>
                    <p><a href="#">link</a></p>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Now, we can install a browser-sync which is a webserver that will also auto reload the page when we change our files.

npm i --save-dev browser-sync

And in devDependencies you should have:

# in package.json you should have
"devDependencies": {
    "browser-sync": "^2.27.9",
    "node-sass": "^7.0.1"
}

Now, let’s add a serve script to our package.json in a way we can start browser-sync with npm run serve in cli.

# In package.json
"scripts": {
    "build": "node-sass scss/styles.scss css/styles.css",
    "serve": "browser-sync start --server --files=\"css/*.css,index.html\""
},

As you can see browser-sync has a --files flag used to define which files to watch for changes to trigger the auto-reload.

In this case, we’re watching all the css files in the css folder (css/*.css) and index.html.

When you npm run serve you should have:

➜ npm run serve

> bootstrap5-theme@1.0.0 serve
> browser-sync start --server --files='css/*.css,index.html'

[Browsersync] Access URLs:
 ------------------------------------
       Local: http://localhost:3001
    External: http://192.168.0.6:3001
 ------------------------------------
          UI: http://localhost:3002
 UI External: http://localhost:3002
 ------------------------------------
[Browsersync] Serving files from: ./
[Browsersync] Watching files...

If so, it means that browser-sync is up and running and you should be able to access index.html from http://localhost:3002/.

index.html should be styled as per css/styles.css

Note: You can stop browser-sync with ctrl+c.

In addition, the page should automatically refresh every time we change index.html or css/styles.css (as it is, you’ll have to use npm run build to change styles.css).

Automate scss build

It’s the moment to automate the build of our styles.css. For this purpose will use chokidar, which will trigger npm run build every time we change a scss file.

npm i --save-dev chokidar-cli

In devDependencies you should have:

# in package.json you should have
"devDependencies": {
    "browser-sync": "^2.27.9",
    "chokidar-cli": "^3.0.0",
    "node-sass": "^7.0.1"
}

Let’s add a watch script to our package.json in a way we can build styles.css when we change a scss file. Use npm run watch then to start watching the files.

# In package.json
"scripts": {
    "build": "node-sass scss/styles.scss css/styles.css",
    "serve": "browser-sync start --server --files=\"css/*css,index.html\"",
    "watch": "chokidar \"scss/*.scss\" -c \"npm run build\""
},

Last touch, let’s install npm-run-all to polish the start script so we can run npm run serve and npm run watch on a single command (which is going to be npm start).

npm i --save-dev npm-run-all

And in devDependencies you should have:

# in package.json you should have
"devDependencies": {
    "browser-sync": "^2.27.9",
    "chokidar-cli": "^3.0.0",
    "node-sass": "^7.0.1",
    "npm-run-all": "^4.1.5"
}

The start script now.

# In package.json
"scripts": {
    "start": "npm-run-all --parallel watch serve",
    "build": "node-sass scss/styles.scss css/styles.css",
    "serve": "browser-sync start --server --files=\"css/*css,index.html\"",
    "watch": "chokidar \"scss/*.scss\" -c \"npm run build\""
},

If you still here?! Congratulation! And if running npm start and you got browser-sync and the watch task running you’re a legend!

We’re finally able to touch some scss now!

Add a typeface

Ok! We’re ready to add our typeface. In this case, I’ll use Lato.

Download and place the woff, woff2 fonts in a fonts folder.

How to get woff and woff2 files?

Most of the free font providers (eg: fontsquirrel, googlefonts) provide ttf file format which is not ideal for the web.

So you’ll need to convert the ttf to woff2. To do that, you can use pages like Font Squirrel Webfont Generator or similar.

Let’s create scss/_fonts.scss.

touch scss/_fonts.scss

Which has to be imported at the beginning of scss/styles.scss.

After that you should have:

// scss/styles.scss
@import "fonts";
@import "./node_modules/bootstrap/scss/functions";
@import "variables";
.
.
.

Now, let’s add our @font-face declaration in scss/_fonts.scss

/* lato-regular - latin */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 400;
  src: local(''),
       url('../fonts/lato-v22-latin-regular.woff2') format('woff2'), 
       url('../fonts/lato-v22-latin-regular.woff') format('woff');
  font-display: swap;
}

/* lato-700 - latin */
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 700;
  src: local(''),
       url('../fonts/lato-v22-latin-700.woff2') format('woff2'), 
       url('../fonts/lato-v22-latin-700.woff') format('woff');
  font-display: swap;
} 

@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 300;
  src: local(''),
       url('../fonts/lato-v22-latin-300.woff2') format('woff2'), 
       url('../fonts/lato-v22-latin-300.woff') format('woff');
  font-display: swap;
}

/* lato-italic - latin */
@font-face {
  font-family: 'Lato';
  font-style: italic;
  font-weight: 400;
  src: local(''),
       url('../fonts/lato-v22-latin-italic.woff2') format('woff2'), 
       url('../fonts/lato-v22-latin-italic.woff') format('woff');
  font-display: swap;
}

Anti aliasing:

Currently antialiasing is out of specs for css3 but some browsers do implement it and sometimes you may want to boost font rendering.

html * {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale
}

At this point you should have the following folder structure:

bootstrap5-theme
├── node_modules
├── .nvmrc
├── .gitignore
├── package-lock.json
├── package.json
├── css
│   └── styles.css
├── fonts
│   ├── lato-v22-latin-300.woff
│   ├── lato-v22-latin-300.woff2
│   ├── lato-v22-latin-regular.woff
│   ├── lato-v22-latin-regular.woff2
│   ├── lato-v22-latin-italic.woff
│   ├── lato-v22-latin-italic.woff2
│   ├── lato-v22-latin-700.woff
│   └── lato-v22-latin-700.woff2
├── scss
│   ├── _variables.scss
│   └── styles.scss   
└── index.html

Last but not least, let’s edit scss/_variables.scss and set the default font to Lato changing $font-family-base.

$font-family-base it is used to setup a css variable --bs-font-family-base in :root within node_modules/bootstrap/scss/_root.scss.

--bs-font-family-base is then used within node_module/bootstrap/scss/_reboot.scss in the body.

$font-family-base:            Lato, arial, sans-serif;
.
.
.

You should have your theme using Lato. If that is so, congratulation! You finally changed a variable in Bootstrap 5 and built it!

woff2 and woff:

We used to have many formats to consume fonts on the web (ttf, eot, woff, svg) but nowadays woff2 and woff will suffice.

See Why should we include ttf, eot, woff, svg,… in a font-face

Preload fonts for production:

If you were in production to speed up font loading you would use a <link rel="preload"> in your <head> html tag.

<link 
  rel="preload" 
  href="fonts/lato-v22-latin-700.woff2" 
  as="font" 
  type="font/woff2" 
  crossorigin />

See Preload web fonts to improve loading speed for more details.

Use a modular scale

As the readme of Modular Scale state: A modular scale is a list of values that share the same relationship.

Which it means that we’re going to use modular-scale to calculate for us some ratios, some numbers in a way that our typography looks harmonious.

npm i modular-scale

And then let’s import at the beginning of scss/styles.scss

// scss/styles.scss
@import "./node_modules/modularscale-sass/stylesheets/modularscale";
@import "fonts";
@import "./node_modules/bootstrap/scss/functions";
.
.
.

In this case, I’ve opted to use 1.618 as ratio and (1rem, 1.25rem, 1.5rem, 1.75rem) as base, see the scale.

Now, I’ll add the $modularscale at the beginning of scss/_variables.scss:

$modularscale: (
  base: 1rem 1.25rem 1.5rem 1.75rem,
  ratio: 1.618
);

So, in theory, we should be able to use the ms function from modular-scale.

Inline text elements

Let’s start with the inline text elements, which are the a tags (links) and the lead text.

As a plus, we’ll set the body background color and body color. We’ll pick the colors from the ones provided by Bootstrap.

In scss/_variables.scss let’s change the following:

Body settings: will have white background and $gray-900 text-color

$body-bg:                   $white;
$body-color:                $gray-900;

Links: will use indigo color (respectively $indigo-500 and $indigo-300 on hover) for the rest will leave the default values.

$link-color:                              $indigo-500;
$link-decoration:                         underline;
$link-shade-percentage:                   20%;
$link-hover-color:                        $indigo-300;
$link-hover-decoration:                   null ;

For the lead, will only change the font size, using the ms function from our modular scale (In this case ms(3) which is 1.5rem).

$lead-font-size:              ms(3);
$lead-font-weight:            300;

These variables, they going to be used in:

!default

We did remove the !default keyword. In theory, we could have left it there, it would have not changed the current behaviour.

But, as we are the one who is changing the default values to a defined value, I think it’s wiser and clear to state a) we did change that value (as the rest of the variables still have the !default) b) we’re not expecting any extra override.

Also, we could have used a different _custom_variable.scss on top of the current imports within styles.scss and simply override the variables we wanted to use instead to copy _variables.scss from Bootstrap.

I think that having the copy facilitates wandering around and curiosity (hopefully you’ll read and edit more variables than what we use in this tutorial).

Displays and headings

Ok, displays and headings at last!

In general, we’re going to change the font sizes using our modular scale and we’re going to use $purple-900 as heading color.

Note: The display color is not set on the .display class but is inherited by the tag used. Which it means: <div class="display-1"> will be $gray-900, while <h1 class="display-1"> will be $purple-900.

In scss/_variables.scss let’s change the following:

Headings: font size / modular scale and color

$h1-font-size:                ms(8);
$h2-font-size:                ms(7);
$h3-font-size:                ms(6);
$h4-font-size:                ms(5);
$h5-font-size:                ms(3);
$h6-font-size:                ms(1);

$font-sizes: (
  1: $h1-font-size,
  2: $h2-font-size,
  3: $h3-font-size,
  4: $h4-font-size,
  5: $h5-font-size,
  6: $h6-font-size
);

$headings-color:              $purple-900;

Displays: font size / modular scale.

$display-font-sizes: (
  1: ms(14),
  2: ms(13),
  3: ms(12),
  4: ms(11),
  5: ms(10),
  6: ms(9)
);

These variables will affect the following:

Quick recap:

  1. we did extend our codebase with browser-sync, chokidar-cli and npm-run-all
  2. then we installed modular-scale and setup the font
  3. we changed the variables for the inline text elements
  4. we changed the variables for the displays and heading

We started the customisation finally! We’ve got the base font, some inline text elements, headings and displays.

Congratulation! Our theme start to get shape!

But, most importantly we wondered around scss/_variables.scss, _reboot.scss and _type.scss and hopefully you played a little bit more with other variables too.

As usual, curiosity is a must!

Notes

This post was partially inspired by bootstrap-npm-starter.