EthicsComputerCourses
Join training +91 90532 10052

The Utility API in Depth

Bootstrap · Lesson 34 of 40 Advanced 8 min read Updated

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:

NeedBuilt-in classes
Stacking orderz-n1, z-0, z-1, z-2, z-3
Images that fill a boxobject-fit-cover, object-fit-contain (also responsive: object-fit-md-cover)
Link underlineslink-offset-2, link-underline-opacity-25, link-underline-opacity-100-hover
Custom focus outlinefocus-ring, focus-ring-success
Gaps in one directionrow-gap-3, column-gap-2
Textfw-medium, text-wrap, text-nowrap, text-truncate, lh-sm
Interactionuser-select-none, user-select-all, pe-none (ignore clicks)
Overflow and heightoverflow-x-auto, overflow-y-auto, vh-100, min-vh-100
HTMLTry it yourself
<!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:

OptionDefaultWhat it does
propertyrequiredThe CSS property, or a list of properties set to the same value
valuesrequiredA list (auto pointer) or a map ((sm: .5rem, lg: 2rem)); map keys become class suffixes
classthe property nameThe class prefix, so class: cursor gives .cursor-pointer
responsivefalseAlso generate -sm-, -md- ... versions inside media queries
statenonePseudo-classes such as hover focus; adds .opacity-50-hover:hover style classes
css-varfalseSet a CSS variable instead of a property
css-variable-namethe class nameThe variable name used with css-var (Bootstrap adds --bs-)
local-varsnoneExtra CSS variables written inside each generated rule
rfsfalseMake the value scale fluidly with RFS, Bootstrap's responsive font-size engine
printfalseAlso generate -print- classes inside @media print
rtltrueSet 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):

SCSS
@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 last

Step 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:

SCSS
$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:

SCSS
"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:

SCSS
"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):

CSS
.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:

HTMLTry it yourself
<!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":

SCSS
// 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 $utilities before importing utilities. The map does not exist yet, so Sass fails or your map is overwritten. Edit it after utilities and before utilities/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-merge on values without map-get. Writing "width": (values: (10: 10%)) throws away property and 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-cover and focus-ring are often reinvented.

Key points

  • All utility classes come from the $utilities Sass map, turned into CSS by utilities/api.
  • Each entry needs property and values; class, responsive, state, css-var, local-vars, rfs, print and rtl are optional.
  • Edit the map after importing utilities and before utilities/api, using map-merge, map-get and map-remove.
  • state: hover produces classes like .opacity-50-hover:hover; responsive: true adds breakpoint versions.
  • css-var: true utilities set --bs-* variables that your own components can read.
  • $enable-important-utilities controls !important for 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
SCSS
$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?

2. What does state: hover add to a utility?

3. How do you remove Bootstrap's float utilities from your build?

4. A utility has css-var: true and class: tint. What does .tint-saffron do?

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