Every utility class Bootstrap ships is generated from one Sass map, $utilities. Each entry describes a family of classes — the property it sets, the values it accepts, and which variants to generate. Customizing utilities means editing that map, not writing CSS.
Workflow
- Step 1: Understand how generation works
- Step 2: Read a utility definition
- Step 3: Add a utility
- Step 4: Modify, rename, or remove a default utility
- Step 5: Generate responsive, state, print, and dark variants
- Step 6: Compose values with custom properties and advanced selectors
- Step 7: Trim the generated CSS
- Step 8: Add a utility to Bootstrap itself
- Step 9: Verify
Step 1: Understand how generation works
scss/utilities/_api.scss is the whole pipeline:
@include generate-utility-at-properties($utilities);
@layer utilities {
@include generate-utilities-loop($utilities, $breakpoints);
}
Three consequences worth knowing before you touch anything:
- Utilities are the last cascade layer, so they win over component styles without needing
!important. Only groups that opt in withimportant: trueadd it. @propertyregistration happens outside the layer. Every custom property a utility assigns (--bg,--fg,--bc,--rounded-size, …) is registered withsyntax: "*"; inherits: false, so applying.border-inversedoesn't leak its composed value into descendants. New utilities that assign custom properties get this automatically.- The map is configured on the
configmodule, notbootstrap.$utilitiesis declared inscss/_config.scssand only populated inscss/_utilities.scss(which loads config with@use "config" as *). Configuring the wrong module fails:
// Does NOT work — "This variable was not declared with !default in the @used module."
@use "bootstrap/scss/bootstrap" with (
$utilities: (…)
);
// Works — configure config first, then load Bootstrap
@use "bootstrap/scss/config" with (
$utilities: (…)
);
@use "bootstrap/scss/bootstrap";
The same applies to any variable that lives in _config.scss ($radius, $spacers, $breakpoints, $zindex-levels). Variables declared in a partial that bootstrap.scss forwards — $root-tokens, $colors, $blue — can be configured on bootstrap directly.
Step 2: Read a utility definition
A group is a map. The simplest form needs two keys:
"overflow": (
property: overflow,
values: auto hidden visible scroll,
),
The full option set, as validated by generate-utility():
| Key | Required | What it does |
|---|---|---|
property |
Yes | Property name, space-separated list of names, or a map of property-to-value pairs |
values |
Yes | List (class suffix equals the value) or map (keys become suffixes; a null key omits the suffix) |
class |
No | Class name to use instead of the property name |
selector |
No | class (default), attr-starts, or attr-includes |
child-selector |
No | Descendant selector, wrapped in :where() for zero specificity |
variables |
No | Custom properties to emit — a map of static values, or a list that each receive the utility value |
state |
No | Pseudo-class variants to generate, e.g. hover focus |
responsive |
No | Generate a class per breakpoint (default false) |
print |
No | Generate print:-prefixed classes (default false) |
dark |
No | Generate dark:-prefixed classes inside prefers-color-scheme: dark (default false) |
important |
No | Append !important (default false) |
enabled |
No | Set false to suppress output while keeping the definition (default true) |
The generator validates as it goes, which makes mistakes cheap to find: a missing property or values is a hard @error, a non-boolean responsive is a hard @error, and a misspelled key is a @warn listing the valid ones.
Step 3: Add a utility
Your map is merged over Bootstrap's defaults, so pass only what's new:
@use "bootstrap/scss/config" with (
$utilities: (
"cursor": (
property: cursor,
class: cursor,
responsive: true,
values: auto pointer grab,
),
)
);
@use "bootstrap/scss/bootstrap";
That yields .cursor-auto, .cursor-pointer, .cursor-grab, plus .sm:cursor-* through .2xl:cursor-*.
Value shapes control the class names:
// List — suffix is the value itself: .cursor-pointer
values: auto pointer grab,
// Map — keys become suffixes: .tint-brand
values: (brand: var(--purple-500), muted: var(--gray-200)),
// null key — no suffix at all: .shout
values: (null: uppercase),
Step 4: Modify, rename, or remove a default utility
The merge is shallow: supplying an existing group key replaces that entire group. Overriding one option and omitting the rest fails loudly —
// Errors: "Utility is missing required `property` key"
"overflow": (
responsive: true,
values: visible hidden scroll auto,
),
so copy the full definition from scss/_utilities.scss and add your change:
@use "bootstrap/scss/config" with (
$utilities: (
// Make overflow responsive
"overflow": (
property: overflow,
values: auto hidden visible scroll,
responsive: true,
),
// Remove a family entirely
"width": null,
// Rename .ms-* to v4-style .ml-*: drop the original, add a replacement
"margin-start": null,
"margin-start-alias": (
property: margin-inline-start,
class: ml,
responsive: true,
values: (0: 0, 1: var(--spacer-1), 2: var(--spacer-2), auto: auto),
),
)
);
@use "bootstrap/scss/bootstrap";
Note the CSS tokens in that last group. The file configuring config can't read Bootstrap's Sass members — $spacers, $negative-spacers, and helpers like map-merge-multiple() aren't in scope yet — so restate values as CSS tokens (var(--spacer-2), which tracks the spacer scale) or as literals.
To extend an existing family without copying its definition, register a new group that reuses the same class:
"width-extra": (
property: width,
class: w,
responsive: true,
values: (10: 10%, 15: 15%),
),
This adds .w-10 and .w-15 next to Bootstrap's .w-25 … .w-100, and is usually the better choice — it survives Bootstrap updates that change the original group.
Prefer enabled: false over null when you want the definition to stay visible in the map (useful in a shared config where the group is documented but intentionally off).
Step 5: Generate responsive, state, print, and dark variants
v6 uses prefixes, not infixes or suffixes. All four variant types compose with the base class name:
| Option | Generated class | Notes |
|---|---|---|
responsive: true |
.md:cursor-pointer |
One pass per key in $breakpoints; written .md\: in the CSS |
state: hover focus |
.hover:cursor-pointer |
Selector is .hover\:cursor-pointer:hover |
print: true |
.print:d-none |
Wrapped in @media print |
dark: true |
.dark:bg-black |
Wrapped in @media (prefers-color-scheme: dark) |
responsive and state stack, producing .md:hover:cursor-pointer. Each variant multiplies the generated CSS, so enable them per group rather than globally.
Step 6: Compose values with custom properties and advanced selectors
Most color-ish utilities don't set the property directly. They assign a local custom property and then consume it, which is what makes opacity and composition work:
"tint": (
property: (
"--tint": null,
"background-color": var(--tint),
),
class: tint,
values: (brand: var(--purple-500)),
),
A null value in the property map means "receive the utility's value"; every other entry is emitted as written. The above generates:
.tint-brand {
--tint: var(--purple-500);
background-color: var(--tint);
}
Related mechanics:
variablesemits extra custom properties. As a map, each gets a static value on every class in the group; as a list, each receives the utility's value.- Property maps plus map values. When
propertyis a map and avaluesentry is itself a map keyed by property name, each property picks its own value — useful for a group that sets several related properties at different values. selector: attr-starts/attr-includestargets[class^="…"]/[class*="…"]instead of a class, and requires aclasskey. Bootstrap uses it so.ratio-16x9can feedaspect-ratiothrough--ratio.child-selectorwraps the rule in:where(), e.g.:where(.stack-gap-1 > *). Zero specificity means a child's own styles still win.
Custom property names are written unprefixed in Sass. Bootstrap's dist and CDN CSS run PostCSS to add --bs-, so compiling the source yourself keeps them unprefixed.
Step 7: Trim the generated CSS
Utilities are the biggest slice of Bootstrap's CSS, and the map is the cheapest place to cut:
-
Start from the utilities-only entry point if you don't need components:
@use "bootstrap/scss/config" with ($utilities: (…)); @use "bootstrap/scss/bootstrap-utilities"; -
Set unused groups to
null(orenabled: false) rather than relying on the minifier or a purge step. -
Turn
responsiveoff on groups you only use at one breakpoint — a responsive group costs six copies of itself. -
Narrow
valuesmaps instead of deleting whole groups when you only need a few steps.
See Optimize for the wider picture, including Lightning CSS and purging.
Step 8: Add a utility to Bootstrap itself
When contributing a utility to the framework rather than a project:
- Add the group to the
$utilitiesmap inscss/_utilities.scss, near related groups, wrapped in// scss-docs-start utils-<name>/// scss-docs-end utils-<name>markers. - Prefer composing through a custom property when the utility should interact with opacity or theme colors, and reuse existing token values (
$spacers,$util-opacity,theme-color-refs()) instead of literals. - Document it on the matching page in
site/src/content/docs/utilities/, rendering the definition with<ScssDocs name="utils-<name>" file="scss/_utilities.scss" />, and add asite/data/sidebar.ymlentry if the page is new. - Add coverage in
scss/tests/utilities/_api.test.scss, which calls the generator mixins directly with a fixture map. - Run
npm run css. Itscss-docsstep regeneratesdist/css/bootstrap-utilities.metadata.json, which the docs read — commit it alongside your change.
Step 9: Verify
npm run css-lint, thennpm run css.- Grep the compiled CSS for the classes you expected, including the escaped variants (
\.md\\:cursor-pointer), and confirm nothing you removed is still present. - Confirm new custom properties appear in the
@propertyblock at the top of the file — if one is missing, the utility isn't assigning it throughpropertyorvariables. - Watch the Sass output for
@warnlines about unknown keys; they're the fastest signal that an option was misspelled and silently ignored. - Check specificity in the browser: utilities should override component styles through the layer order alone, without
!important.