How to make a Bootstrap 5 theme: customise utility classes
Now that our typography setup is done, we can indulge in customising some Bootstrap utility classes.
The scope of this tutorial is to learn and start using the utility class API generator Bootstrap 5 provides.
The general idea
The idea behind utility classes is to have an immutable, composable, predictable set of css classes to avoid side-effects as much as possible.
Bootstrap — like other css frameworks — provides a set of utility classes out of the box, but allows you to add, remove and change them as you need.
The main files related to the utility classes are:
node_modules/bootstrap/scss/mixins/_utilities.scsswhich contains thegenerate-utilitymixin. This file is imported bynode_modules/bootstrap/scss/_mixins.scssnode_modules/bootstrap/scss/_utilities.scss, which defines the$utilitiesvariablenode_modules/bootstrap/scss/utilities/_api.scsswhich supports utility generation withbreakpoints,rfsandprintincluded. This is the file imported at the end of ourstyles.scss.
In a way to customise the utility classes, we’ll make a copy of the scss/_utilities.scss and import it in our styles.scss.
// Loading flow
1. import bootstrap/scss/mixins/_utilities.scss
this will make the generate-utility mixin available
2. import bootstrap/scss/_utilities.scss
this will create the $utilities map
3. import bootstrap/scss/utilities/_api.scss
which uses the $utilities map and generate-utility to actually
generate the css classes
Sass maps
Before we investigate further, we have to explain a little about sass maps so you can make sense of the Bootstrap files involved.
Map basics:
// Scss basic syntax
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
// Just to give some mental model this would be the
// the same map if it was written in javascript (as object)
var fontWeights = {"regular": 400, "medium": 500, "bold": 700};
Iterate on a map:
// Assuming the following map
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
// Iterate on the map
@each $name, $value in $font-weights {
.fw-#{$name} {
font-weight: $value;
}
}
// Which will generate
.fw-regular {
font-weight: 400;
}
.fw-medium {
font-weight: 500;
}
.fw-bold {
font-weight: 700;
}
Join two maps:
$font-weights-base: ("regular": 400, "medium": 500, "bold": 700);
$font-weights-exte: ("lighter": 100, "bolder": 900);
$font-weights: map-merge($font-weights-base, $font-weights-exte);
// $font-weights content will be
("regular": 400, "medium": 500, "bold": 700, "lighter": 100, "bolder": 900);
Get a value for a specific key:
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
map-get($font-weights, "medium"); // 500
Set a value for a specific key:
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
map-set($font-weights, "yadda", 20);
// $font-weights content will be
// ("regular": 400, "medium": 500, "bold": 700, "yadda": 20);
Case node-sass vs dart-sass
While originally built with ruby, sass has been for a long time developed in C/C++ (LibSass and node-sass) but it is now moving to Dart (dart-sass).
As the writing of this tutorial, LibSass and node-sass are deprecated.
With dart-sass though a new module system is added and for this reason, some newer syntax in the official docs may need the map module imported to work.
E.g.: map-get vs map.get
// previous example with the `.` syntax
@use "sass:map";
map.get($font-weights, "medium");
See: “Sass map.get() doesn’t work. map-get() does. What gives?”.
Basic setup
There are several approaches we could use to customize our utility classes.
Approaches to customize utility classes:
- extend
$utilitiesafter our variables - copy the whole
node_modules/bootstrap/scss/_utilities.scssto ourscssfolder
Extend $utilities
As the variable $utilities defined in node_modules/bootstrap/scss/_utilities.scss has a !default keyword we could potentially create a new $utilities before importing node_modules/bootstrap/scss/_utilities.scss and that would be merged with the rest of the values.
Another way would be using map-merge after @import "bootstrap/scss/utilities";.
In both scenarios, we try to alter the default map.
Despite being the methodology proposed in the Bootstrap 5 documentation I think there may be some inconvenience.
Firstly, you can not read all the utility classes at once, as part of the definition would stay in node_modules/bootstrap/scss/mixins/_utilities.scss and the other part where we use the map-merge.
Secondly, if you have to modify or remove some utility the experience can be a little bit unpleasant as you may need to use a combination of map-merge and map-get.
Copy node_modules/bootstrap/scss/_utilities.scss
For these reasons, we’ll copy node_modules/bootstrap/scss/_utilities.scss instead to our scss folder and start editing directly so we’ll have all the utilities in one place and no map-merge required.
The drawback of this approach is that in case of a breaking change a mere npm update would not suffice as we’ll need to re-sync _utilities.scss with the latest Bootstrap version.
Assuming minor changes (in the semver sense) in theory the data structure should only expand leaving our code working but possibly without the latest feature.
Drawback Example:
In Bootstrap 5.1.3
https://github.com/twbs/bootstrap/blob/v5.1.3/scss/_utilities.scss#L138
"border-color": (
property: border-color,
class: border,
values: map-merge($theme-colors, ("white": $white))
),
In Bootstrap 5.2.0
https://github.com/twbs/bootstrap/blob/v5.2.0/scss/_utilities.scss#L136
"border-color": (
property: border-color,
class: border,
local-vars: (
"border-opacity": 1
),
values: $utilities-border-colors
),
In this case, except we manually update `border-color` we would loose
local-vars and the new maps in values.
Considering all of that, I think is still a reasonable tradeoff to make a copy because I’m assuming I’ll spend more time on writing utilities than updating Bootstrap. In addition, forcing myself to do the manual update may inspire new changes for other customised utilities.
cp node_modules/bootstrap/scss/_utilities.scss scss/_utilities.scss
At the end of this commands you should have:
bootstrap5-theme
├── node_modules
├── .nvmrc
├── .gitignore
├── package-lock.json
├── package.json
└── scss
├── _utilities.scss
├── _variables.scss
└── styles.scss
and then in our styles.scss.
Change from
@import "./node_modules/bootstrap/scss/utilities";
To
@import "utilities";
You should have:
// Configuration
@import "./node_modules/bootstrap/scss/functions";
@import "variables";
@import "./node_modules/bootstrap/scss/mixins";
@import "utilities";
// Layout & components
@import "./node_modules/bootstrap/scss/root";
@import "./node_modules/bootstrap/scss/reboot";
@import "./node_modules/bootstrap/scss/type";
@import "./node_modules/bootstrap/scss/images";
@import "./node_modules/bootstrap/scss/containers";
@import "./node_modules/bootstrap/scss/grid";
@import "./node_modules/bootstrap/scss/tables";
@import "./node_modules/bootstrap/scss/forms";
$utilities in scss/_utilities
So, as said, $utilities is a map and it is used in combination with the generate-utility mixin to generate the csss classes.
Let’s take in consideration the following:
$utilities: () !default;
$utilities: map-merge(
(
"opacity": (
property: opacity,
values: (
0: 0,
25: .25,
50: .5,
75: .75,
100: 1,
)
),
.
.
.
"display": (
responsive: true,
print: true,
property: display,
class: d,
values: inline inline-block block grid table table-row table-cell flex inline-flex none
)
),
$utilities
);
As key we have the name of the css property. In this case "opacity" and "display".
Reading at _api.scss I can not see anywhere the key is used. Thus, I would conclude that the key would be merely used in case of override and in case map-get is needed as shown in the official documentation.
The value of the property is another map. "opacity" has only property and values, while "display" has responsive, print, property, class and values.
How is the class name composed within generate-utility?
If we check mixins/_utilities.scss#L64 we can see that the class name is composed by .#{$property-class + $infix + $property-class-modifier}.
// assuming opacity-md-50
.#{$property-class + $infix + $property-class-modifier}
^ ^ ^
| | |
opacity md 50
$property-class
$property-class will use the class property if present, else it will use the property key.
// https://github.com/twbs/bootstrap/blob/v5.1.3/scss/mixins/_utilities.scss#L20
// Use custom class if present
$property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1));
$property-class: if($property-class == null, "", $property-class);
$property-class examples
E.g. "opacity"
all the classes will be `.opacity-0, .opacity-25 ...` because it has only the `property` key.
"opacity": (
property: opacity, <<<< IT WILL BE USED
values: (
0: 0,
25: .25,
50: .5,
75: .75,
100: 1,
)
),
E.g. "display"
all the classes will be `.d-inline, .d-block ...` because it has the `class` key.
"display": (
responsive: true,
print: true,
property: display,
class: d, <<<< IT WILL BE USED
values: inline inline-block block grid table table-row table-cell flex inline-flex none
)
$property-class-modifier
For $property-class-modifier, if values is a list will use directly, if values is a map it will use the key.
// https://github.com/twbs/bootstrap/blob/v5.1.3/scss/mixins/_utilities.scss#L64
// get the values
$values: map-get($utility, values);
// If the values are a list or string, convert it into a map
@if type-of($values) == "string" or type-of(nth($values, 1)) != "list" {
$values: zip($values, $values);
}
.
.
.
@each $key, $value in $values {
.
.
.
// Don't prefix if value key is null (eg. with shadow class)
$property-class-modifier: if($key, if($property-class == "" and $infix == "", "", "-") + $key, "");
.
.
.
.#{$property-class + $infix + $property-class-modifier} {
@each $property in $properties {
@if $is-local-vars {
@each $local-var, $value in $is-local-vars {
--#{$variable-prefix}#{$local-var}: #{$value};
}
}
#{$property}: $value if($enable-important-utilities, !important, null);
}
}
}
$property-class-modifier examples
E.g. "opacity"
`values` is a map. The map `keys` are `0, 25, 50, 75, 100`, while the map `values` are `0, .25, .5, .75, 1`
"opacity": (
property: opacity, <<<< IT WILL BE USED
values: (
0: 0,
25: .25,
50: .5,
75: .75,
100: 1,
)
),
.opacity-0 {
opacity: 0;
}
.opacity-25 {
opacity: .25;
}
.opacity-50 {
opacity: .5;
}
.
.
.
E.g. "display"
`values` is a list. it will be used to generate $property-class-modifier and $value
"display": (
responsive: true,
print: true,
property: display,
class: d, <<<< IT WILL BE USED
values: inline inline-block block grid table table-row table-cell flex inline-flex none
)
.d-inline {
display: inline;
}
.d-inline-block {
display: inline-block;
}
.
.
.
$infix, print and responsive
As per utilities/_api.scss#L12 when responsive it uses @include media-breakpoint-up and injects xs, sm, md, lg, xl, xxl as defined in _variables.scss as $infix to generate the responsive classes.
This will generate classes like .d-md-none, .d-lg-none, .d-xl-none and so on.
While for the print key, as per utilities/_api.scss#L39, @media print is used with a print infix.
This will generate classes like .d-print-none, .d-print-inline, .d-print-block.
Let’s add our first utility
So, we’re ready to add our first utility. We’ll add some classes to control the z-index property.
The classes will be .z- with a value from 0 to 5.
// in our local _utilities.scss
// let's add
"z-index": (
property: z-index,
class: z,
values: 0 1 2 3 4 5
),
so, now you should have
1 // stylelint-disable indentation
2
3 // Utilities
4
5 $utilities: () !default;
6 // stylelint-disable-next-line scss/dollar-variable-default
7 $utilities: map-merge(
8 (
9 "z-index": (
10 property: z-index,
11 class: z,
12 values: 0 1 2 3 4 5
13 ),
14 // scss-docs-start utils-vertical-align
15 "align": (
16 property: vertical-align,
17 class: align,
18 values: baseline top middle bottom text-bottom text-top
19 ),
20 // scss-docs-end utils-vertical-align
21 // scss-docs-start utils-float
22 "float": (
23 responsive: true,
24 property: float,
25 values: (
26 start: left,
27 end: right,
28 none: none,
29 )
30 ),
.
.
.
.
if now you do a npm run build you should have in css/styles.css.
.z-0 {
z-index: 0 !important; }
.z-1 {
z-index: 1 !important; }
.z-2 {
z-index: 2 !important; }
.z-3 {
z-index: 3 !important; }
.z-4 {
z-index: 4 !important; }
.z-5 {
z-index: 5 !important; }
Quick recap:
- we did explore sass maps
- we copied the
_utilities.scssfile in local - we explored the structure of the
$utilities, in particularproperty,values,classandresponsive - we created a new utility for
z-index
Of particular importance, we saw the structure of $utilities and how the css class name is formed from the values in property, values, class and responsive.
With this basic knowledge of the Bootstrap 5 utility API, you should be comfortable in adding new utilities and exploring other options available as rfs, rtl, css-var the API support.