Use, extend, and customize Bootstrap 6's utility API. Use when adding a custom utility class, modifying or removing Bootstrap's default utilities, enabling responsive or state variants, or trimming the generated utility CSS.
php artisan boost:add-skill twbs/bootstrap
Save this content to: .claude/skills/bootstrap-utility-api/SKILL.md
# Bootstrap 6 utility API
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:
```scss
@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 with `important: true` add it.
- **`@property` registration happens outside the layer.** Every custom property a utility assigns (`--bg`, `--fg`, `--bc`, `--rounded-size`, …) is registered with `syntax: "*"; inherits: false`, so applying `.border-inverse` doesn't leak its composed value into descendants. New utilities that assign custom properties get this automatically.
- **The map is configured on the `config` module, not `bootstrap`.** `$utilities` is declared in `scss/_config.scss` and only *populated* in `scss/_utilities.scss` (which loads config with `@use "config" as *`). Configuring the wrong module fails:
```scss
// 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:
```scss
"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:
```scss
@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:
```scss
// 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 —
```scss
// 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:
```scss
@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`:
```scss
"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:
```scss
"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:
```css
.tint-brand {
--tint: var(--purple-500);
background-color: var(--tint);
}
```
Related mechanics:
- **`variables`** emits 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 `property` is a map and a `values` entry 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-includes`** targets `[class^="…"]` / `[class*="…"]` instead of a class, and requires a `class` key. Bootstrap uses it so `.ratio-16x9` can feed `aspect-ratio` through `--ratio`.
- **`child-selector`** wraps 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:
1. Start from the utilities-only entry point if you don't need components:
```scss
@use "bootstrap/scss/config" with ($utilities: (…));
@use "bootstrap/scss/bootstrap-utilities";
```
2. Set unused groups to `null` (or `enabled: false`) rather than relying on the minifier or a purge step.
3. Turn `responsive` off on groups you only use at one breakpoint — a responsive group costs six copies of itself.
4. Narrow `values` maps instead of deleting whole groups when you only need a few steps.
See [Optimize]([[docsref:/customize/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:
1. Add the group to the `$utilities` map in `scss/_utilities.scss`, near related groups, wrapped in `// scss-docs-start utils-<name>` / `// scss-docs-end utils-<name>` markers.
2. 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.
3. 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 a `site/data/sidebar.yml` entry if the page is new.
4. Add coverage in `scss/tests/utilities/_api.test.scss`, which calls the generator mixins directly with a fixture map.
5. Run `npm run css`. Its `css-docs` step regenerates `dist/css/bootstrap-utilities.metadata.json`, which the docs read — commit it alongside your change.
---
## Step 9: Verify
1. `npm run css-lint`, then `npm run css`.
2. Grep the compiled CSS for the classes you expected, including the escaped variants (`\.md\\:cursor-pointer`), and confirm nothing you removed is still present.
3. Confirm new custom properties appear in the `@property` block at the top of the file — if one is missing, the utility isn't assigning it through `property` or `variables`.
4. Watch the Sass output for `@warn` lines about unknown keys; they're the fastest signal that an option was misspelled and silently ignored.
5. Check specificity in the browser: utilities should override component styles through the layer order alone, without `!important`.
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.
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:
!important. Only groups that opt in with important: true add it.@property registration happens outside the layer. Every custom property a utility assigns (--bg, --fg, --bc, --rounded-size, …) is registered with syntax: "*"; inherits: false, so applying .border-inverse doesn't leak its composed value into descendants. New utilities that assign custom properties get this automatically.config module, not bootstrap. $utilities is declared in scss/_config.scss and only populated in scss/_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.
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.
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),
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).
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.
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:
variables emits 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 is a map and a values entry 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-includes targets [class^="…"] / [class*="…"] instead of a class, and requires a class key. Bootstrap uses it so .ratio-16x9 can feed aspect-ratio through --ratio.child-selector wraps 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.
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 (or enabled: false) rather than relying on the minifier or a purge step.
Turn responsive off on groups you only use at one breakpoint — a responsive group costs six copies of itself.
Narrow values maps instead of deleting whole groups when you only need a few steps.
See Optimize for the wider picture, including Lightning CSS and purging.
When contributing a utility to the framework rather than a project:
$utilities map in scss/_utilities.scss, near related groups, wrapped in // scss-docs-start utils-<name> / // scss-docs-end utils-<name> markers.$spacers, $util-opacity, theme-color-refs()) instead of literals.site/src/content/docs/utilities/, rendering the definition with <ScssDocs name="utils-<name>" file="scss/_utilities.scss" />, and add a site/data/sidebar.yml entry if the page is new.scss/tests/utilities/_api.test.scss, which calls the generator mixins directly with a fixture map.npm run css. Its css-docs step regenerates dist/css/bootstrap-utilities.metadata.json, which the docs read — commit it alongside your change.npm run css-lint, then npm run css.\.md\\:cursor-pointer), and confirm nothing you removed is still present.@property block at the top of the file — if one is missing, the utility isn't assigning it through property or variables.@warn lines about unknown keys; they're the fastest signal that an option was misspelled and silently ignored.!important.How can I help you explore Laravel packages today?