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.scss
which contains thegenerate-utility
mixin. This file is imported bynode_modules/bootstrap/scss/_mixins.scss
node_modules/bootstrap/scss/_utilities.scss
, which defines the$utilities
variablenode_modules/bootstrap/scss/utilities/_api.scss
which supports utility generation withbreakpoints
,rfs
andprint
included. 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
$utilities
after our variables - copy the whole
node_modules/bootstrap/scss/_utilities.scss
to ourscss
folder
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.scss
file in local - we explored the structure of the
$utilities
, in particularproperty
,values
,class
andresponsive
- 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.