The Utility API in Depth
Every utility class you have used, from mt-3 to d-md-flex to text-bg-warning, is generated from one Sass map called $utilities. The utility API is the code that loops over that map and writes the classes. Once you understand it, you can add utilities your design needs (cursor styles, fluid font sizes, brand tints), give existing ones responsive or hover versions, and delete the ones you never use to make the CSS smaller.
Customising with CSS Variables & Sass showed one small example. This lesson goes through the API properly, step by step, and starts with useful utilities that are already built in but often missed.
Utilities you already have
Before writing new utilities, check that Bootstrap 5.3 does not already have one. These are easy to miss:
| Need | Built-in classes |
|---|---|
| Stacking order | z-n1, z-0, z-1, z-2, z-3 |
| Images that fill a box | object-fit-cover, object-fit-contain (also responsive: object-fit-md-cover) |
| Link underlines | link-offset-2, link-underline-opacity-25, link-underline-opacity-100-hover |
| Custom focus outline | focus-ring, focus-ring-success |
| Gaps in one direction | row-gap-3, column-gap-2 |
| Text | fw-medium, text-wrap, text-nowrap, text-truncate, lh-sm |
| Interaction | user-select-none, user-select-all, pe-none (ignore clicks) |
| Overflow and height | overflow-x-auto, overflow-y-auto, vh-100, min-vh-100 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Built-in utilities</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-4">
<div class="position-relative mb-4" style="height: 160px">
<img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Hills near Munnar" class="w-100 h-100 object-fit-cover rounded">
<span class="badge text-bg-dark position-absolute top-0 start-0 m-2 z-1">Trip of the week</span>
</div>
<p>
<a href="#offers" class="link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover">See Kerala offers</a>
</p>
<div class="d-flex flex-wrap column-gap-4 row-gap-2 mb-3">
<span class="fw-medium">Munnar</span><span class="fw-medium">Alleppey</span><span class="fw-medium">Kochi</span><span class="fw-medium">Varkala</span>
</div>
<p class="text-truncate border rounded p-2" style="max-width: 260px">Houseboat stay with traditional Kerala lunch and sunset cruise</p>
<p>Booking code: <span class="user-select-all font-monospace">KRL-2026-4521</span> (one click selects it)</p>
<a href="#book" class="btn btn-light border focus-ring focus-ring-success">Tab to me: green focus ring</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>The shape of a utility
Each entry in $utilities has a key (the utility's name) and a map of options. Only property and values are required:
| Option | Default | What it does |
|---|---|---|
property | required | The CSS property, or a list of properties set to the same value |
values | required | A list (auto pointer) or a map ((sm: .5rem, lg: 2rem)); map keys become class suffixes |
class | the property name | The class prefix, so class: cursor gives .cursor-pointer |
responsive | false | Also generate -sm-, -md- ... versions inside media queries |
state | none | Pseudo-classes such as hover focus; adds .opacity-50-hover:hover style classes |
css-var | false | Set a CSS variable instead of a property |
css-variable-name | the class name | The variable name used with css-var (Bootstrap adds --bs-) |
local-vars | none | Extra CSS variables written inside each generated rule |
rfs | false | Make the value scale fluidly with RFS, Bootstrap's responsive font-size engine |
print | false | Also generate -print- classes inside @media print |
rtl | true | Set to false to leave the utility out of the RTL stylesheet |
There is no per-utility "important" option: every generated rule gets !important, controlled for the whole build by $enable-important-utilities (default true). Utilities use !important on purpose, so that a utility always beats component styles; see Spacing, Sizing & Borders.
Building your own utilities step by step
You need an npm project with Bootstrap and Sass (see Get Started). All the code below goes in one file, src/scss/styles.scss.
Step 1: import in the right order
Your changes to $utilities must come after Bootstrap's utilities file (which defines the map) and before utilities/api (which turns the map into classes):
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
// Steps 2 to 5 go here
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
// ...the components you use...
@import "bootstrap/scss/utilities/api"; // always lastStep 2: add a responsive utility
A product grid where cards are clickable on desktop needs a pointer cursor. map-merge adds your entry to the existing map:
$utilities: map-merge($utilities, (
"cursor": (
property: cursor,
class: cursor,
responsive: true,
values: auto pointer not-allowed
)
));This generates .cursor-auto, .cursor-pointer and .cursor-not-allowed, plus .cursor-md-pointer and the other breakpoint versions.
Step 3: add hover variants and fluid sizes
Add more entries inside the same map-merge call. state: hover doubles each class with a hover version, and a map of values gives you full control of the class names:
"opacity": (
property: opacity,
state: hover,
values: (25: .25, 50: .5, 100: 1)
),
"fs-fluid": (
property: font-size,
class: fs-fluid,
values: (hero: clamp(1.75rem, 1rem + 3vw, 3.5rem))
),Re-using the key "opacity" replaces Bootstrap's own opacity utility with yours, so only 25, 50 and 100 remain. Use a new key if you want to keep the original.
Step 4: utilities that set CSS variables
With css-var: true, the class sets a variable instead of a property. That lets one class feed a value into your own component CSS. local-vars writes helper variables inside each rule:
"brand-tint": (
css-var: true,
css-variable-name: tint,
class: tint,
values: (saffron: #ff9933, green: #138808)
),
"glow": (
property: box-shadow,
class: glow,
local-vars: ("glow-color": "13, 110, 253"),
values: (sm: 0 0 .5rem rgba(var(--bs-glow-color), .4))
),Step 5: check the output
Run npm run build (or the Sass compiler) and look at the CSS. Steps 2 to 4 produce exactly this (breakpoint versions of cursor shortened):
.cursor-pointer { cursor: pointer !important; }
.opacity-50 { opacity: 0.5 !important; }
.opacity-50-hover:hover { opacity: 0.5 !important; }
.fs-fluid-hero { font-size: clamp(1.75rem, 1rem + 3vw, 3.5rem) !important; }
.tint-saffron { --bs-tint: #ff9933; }
.tint-green { --bs-tint: #138808; }
.glow-sm {
--bs-glow-color: 13, 110, 253;
box-shadow: 0 0 0.5rem rgba(var(--bs-glow-color), 0.4) !important;
}
@media (min-width: 768px) {
.cursor-md-pointer { cursor: pointer !important; }
}Notice that the css-var utility has no !important: it only sets a variable. To try the classes without a build, paste the compiled CSS into a <style> element:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Generated utilities</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Output of the utility API from steps 2 to 4 */
.cursor-pointer { cursor: pointer !important; }
.opacity-50 { opacity: 0.5 !important; }
.opacity-100-hover:hover { opacity: 1 !important; }
.fs-fluid-hero { font-size: clamp(1.75rem, 1rem + 3vw, 3.5rem) !important; }
.tint-saffron { --bs-tint: #ff9933; }
.tint-green { --bs-tint: #138808; }
.glow-sm { --bs-glow-color: 13, 110, 253; box-shadow: 0 0 0.5rem rgba(var(--bs-glow-color), 0.4) !important; }
/* Your own component reads the variable set by tint-* */
.ribbon { border-inline-start: .5rem solid var(--bs-tint, #6c757d); }
</style>
</head>
<body>
<div class="container py-4">
<h1 class="fs-fluid-hero">Rohtak Handloom Fair</h1>
<div class="row g-3">
<div class="col-sm-6">
<div class="card ribbon tint-saffron cursor-pointer glow-sm"><div class="card-body">Cotton durries from ₹799</div></div>
</div>
<div class="col-sm-6">
<div class="card ribbon tint-green opacity-50 opacity-100-hover"><div class="card-body">Sold out: hover to read</div></div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>Resize the window: the heading grows and shrinks smoothly. Hover the faded card and it becomes solid.
Changing and removing existing utilities
The same map functions edit what Bootstrap already has. Put these after @import "bootstrap/scss/utilities":
// Add a 10% width (w-10) to the existing width utility
$utilities: map-merge($utilities, (
"width": map-merge(
map-get($utilities, "width"),
(values: map-merge(map-get(map-get($utilities, "width"), "values"), (10: 10%)))
),
// Give borders responsive versions: border-md, border-lg ...
"border": map-merge(map-get($utilities, "border"), (responsive: true)),
));
// Delete utilities you never use
$utilities: map-remove($utilities, "float", "user-select");map-get reads an existing entry, map-merge changes one option and keeps the rest, and map-remove deletes whole entries. Setting an entry to null inside map-merge also removes it. To build without !important everywhere, set $enable-important-utilities: false; before importing variables; do this only if your team has a clear rule for how utilities and component styles should win against each other.
Common mistakes
- Changing
$utilitiesbefore importingutilities. The map does not exist yet, so Sass fails or your map is overwritten. Edit it afterutilitiesand beforeutilities/api. - Forgetting
utilities/api. Without it, no utility classes are generated at all. - Reusing a built-in key by accident. A new
"opacity"entry replaces Bootstrap's whole opacity utility. Pick a unique key unless replacing is the goal. - Using
map-mergeonvalueswithoutmap-get. Writing"width": (values: (10: 10%))throws awaypropertyand the other options. Merge into the existing entry. - Writing a utility for something that exists. Check the built-in list first;
z-1,object-fit-coverandfocus-ringare often reinvented.
Key points
- All utility classes come from the
$utilitiesSass map, turned into CSS byutilities/api. - Each entry needs
propertyandvalues;class,responsive,state,css-var,local-vars,rfs,printandrtlare optional. - Edit the map after importing
utilitiesand beforeutilities/api, usingmap-merge,map-getandmap-remove. state: hoverproduces classes like.opacity-50-hover:hover;responsive: trueadds breakpoint versions.css-var: trueutilities set--bs-*variables that your own components can read.$enable-important-utilitiescontrols!importantfor every utility at once.
Exercise
A food delivery app in Lucknow needs: a cursor utility with only pointer and grab values, responsive; an aspect utility on the aspect-ratio property with values square: 1, wide: 16 / 9 and tall: 3 / 4; and Bootstrap's float utility removed. Write the Sass that goes between @import "bootstrap/scss/utilities" and @import "bootstrap/scss/utilities/api", and list three classes it generates.
Show solution
$utilities: map-merge($utilities, (
"cursor": (
property: cursor,
class: cursor,
responsive: true,
values: pointer grab
),
"aspect": (
property: aspect-ratio,
class: aspect,
values: (square: 1, wide: unquote("16 / 9"), tall: unquote("3 / 4"))
)
));
$utilities: map-remove($utilities, "float");It generates, for example, .cursor-grab, .cursor-lg-pointer and .aspect-wide (aspect-ratio: 16 / 9 !important). A bare 16 / 9 would be treated by Sass as a division (output 1.7777777778, with a deprecation warning), so unquote("16 / 9") keeps it as the text CSS expects. Because the aspect values are a map, the keys square, wide and tall become the class suffixes, and float-start, float-end and float-none disappear from the build.
Quick Quiz
1. Between which two imports must you change the $utilities map?
utilities defines the map and utilities/api turns it into classes.
2. What does state: hover add to a utility?
Each value gets a second class that only applies in the given pseudo-class state.
3. How do you remove Bootstrap's float utilities from your build?
map-remove deletes the entry before the API generates classes.
4. A utility has css-var: true and class: tint. What does .tint-saffron do?
CSS variable utilities set a custom property instead of a normal property.
Learn Bootstrap with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
