]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
v6: More utilities API, docs, tests (#42381)
authorMark Otto <markd.otto@gmail.com>
Tue, 5 May 2026 23:09:38 +0000 (16:09 -0700)
committerGitHub <noreply@github.com>
Tue, 5 May 2026 23:09:38 +0000 (16:09 -0700)
* Utilities API and class improvements

* Expand utility API test coverage

* Update utilities API and border-radius docs

* Fix bundlewatch

* Introduce `@mixin generate-utilities-loop()`

* Add text wrapping utilities in Sass utilities API in the docs

* Copy updates, links, callout for postcss prefix, fix a border-radius

* inline warning

* error over warn

* Fix bundlewatch

* more copy edits

* fix layout and headings

* lint

---------

Co-authored-by: Julien Déramond <juderamond@gmail.com>
12 files changed:
.bundlewatch.config.json
scss/_utilities.scss
scss/mixins/_utilities.scss
scss/tests/jasmine.cjs
scss/tests/mixins/_utilities.test.scss
scss/tests/utilities/_api.test.scss
scss/utilities/_api.scss
site/src/components/PageMeta.astro
site/src/content/docs/getting-started/approach.mdx
site/src/content/docs/utilities/api.mdx
site/src/content/docs/utilities/border-radius.mdx
site/src/content/docs/utilities/text-wrapping.mdx

index 3a39bdd9103f2238ac9c54c965feae116301d173..ab63a36164f1ec929b52ff026f9f14b5f6c81ce4 100644 (file)
@@ -18,7 +18,7 @@
     },
     {
       "path": "./dist/css/bootstrap-utilities.css",
-      "maxSize": "17.75 kB"
+      "maxSize": "18.0 kB"
     },
     {
       "path": "./dist/css/bootstrap-utilities.min.css",
     },
     {
       "path": "./dist/css/bootstrap.css",
-      "maxSize": "48.25 kB"
+      "maxSize": "48.5 kB"
     },
     {
       "path": "./dist/css/bootstrap.min.css",
-      "maxSize": "45.0 kB"
+      "maxSize": "45.25 kB"
     },
     {
       "path": "./dist/js/bootstrap.bundle.js",
index 5b6adb4bbd3bb4d5e05e0dd1ef757cb1929297d7..13fae2ac28af6f4f6483129648281bee083a503c 100644 (file)
@@ -128,12 +128,12 @@ $utilities: map.merge(
       values: $position-values
     ),
     "start": (
-      property: left,
+      property: inset-inline-start,
       class: start,
       values: $position-values
     ),
     "end": (
-      property: right,
+      property: inset-inline-end,
       class: end,
       values: $position-values
     ),
@@ -188,6 +188,7 @@ $utilities: map.merge(
       )
     ),
     "border-y": (
+      class: border-y,
       property: border-block,
       values: (
         null: var(--border-width) var(--border-style) var(--border-color),
@@ -195,6 +196,7 @@ $utilities: map.merge(
       )
     ),
     "border-x": (
+      class: border-x,
       property: border-inline,
       values: (
         null: var(--border-width) var(--border-style) var(--border-color),
@@ -375,8 +377,8 @@ $utilities: map.merge(
       responsive: true,
       property: justify-self,
       values: (
-        start: flex-start,
-        end: flex-end,
+        start: start,
+        end: end,
         center: center,
       )
     ),
@@ -687,19 +689,13 @@ $utilities: map.merge(
       values: lowercase uppercase capitalize
     ),
     // scss-docs-end utils-text-transform
-    // "white-space": (
-    //   property: white-space,
-    //   class: text,
-    //   values: (
-    //     wrap: normal,
-    //     nowrap: nowrap,
-    //   )
-    // ),
+    // scss-docs-start utils-text-wrap
     "text-wrap": (
-      property: white-space,
+      property: text-wrap,
       class: text,
       values: wrap nowrap balance pretty,
     ),
+    // scss-docs-end utils-text-wrap
     // scss-docs-start utils-text-break
     "word-wrap": (
       property: word-wrap word-break,
@@ -709,12 +705,6 @@ $utilities: map.merge(
     // scss-docs-end utils-text-break
     // scss-docs-end utils-text
     // scss-docs-start utils-color
-    // "color-attr": (
-    //   selector: "attr-includes",
-    //   class: "fg-",
-    //   property: color,
-    //   values: var(--fg),
-    // ),
     "fg": (
       property: (
         "--fg": null,
@@ -870,7 +860,10 @@ $utilities: map.merge(
     // scss-docs-end utils-interaction
     // scss-docs-start utils-border-radius
     "border-radius": (
-      property: border-radius,
+      property: (
+        "--rounded-size": null,
+        "border-radius": var(--rounded-size)
+      ),
       class: rounded,
       values: map.merge(
         $radii,
@@ -881,49 +874,61 @@ $utilities: map.merge(
         )
       )
     ),
+    "rounded-size": (
+      property: --rounded-size,
+      class: rounded-size,
+      values: map.merge(
+        $radii,
+        (
+          null: map.get($radii, 5),
+          circle: 50%,
+          pill: var(--radius-pill)
+        )
+      )
+    ),
     "rounded-top": (
       property: border-start-start-radius border-start-end-radius,
       class: rounded-top,
       values: map.merge(
         $radii,
         (
-          null: map.get($radii, 5),
+          null: var(--rounded-size, var(--radius-5)),
           circle: 50%,
           pill: var(--radius-pill)
         )
       )
     ),
     "rounded-end": (
-      property: border-end-end-radius border-end-start-radius,
+      property: border-start-end-radius border-end-end-radius,
       class: rounded-end,
       values: map.merge(
         $radii,
         (
-          null: map.get($radii, 5),
+          null: var(--rounded-size, var(--radius-5)),
           circle: 50%,
           pill: var(--radius-pill)
         )
       )
     ),
     "rounded-bottom": (
-      property: border-end-end-radius border-end-start-radius,
+      property: border-end-start-radius border-end-end-radius,
       class: rounded-bottom,
       values: map.merge(
         $radii,
         (
-          null: map.get($radii, 5),
+          null: var(--rounded-size, var(--radius-5)),
           circle: 50%,
           pill: var(--radius-pill)
         )
       )
     ),
     "rounded-start": (
-      property: border-start-start-radius border-start-end-radius,
+      property: border-start-start-radius border-end-start-radius,
       class: rounded-start,
       values: map.merge(
         $radii,
         (
-          null: map.get($radii, 5),
+          null: var(--rounded-size, var(--radius-5)),
           circle: 50%,
           pill: var(--radius-pill)
         )
index 67045cdce0c36b6ccb84c6860d69b5de4c6e88e4..2d78150d7bff7562badd25c0cf2c41c2621b30c8 100644 (file)
@@ -1,8 +1,7 @@
 @use "sass:list";
 @use "sass:map";
 @use "sass:meta";
-
-// stylelint-disable scss/dollar-variable-pattern
+@use "../layout/breakpoints" as bp;
 
 // Utility generator
 
 //   text-decoration-color: ...;
 // }
 
+// Helper mixin to emit CSS custom properties from a utility's `variables` key.
+// When variables is a map, the provided static values are used on each class.
+// When variables is a list or single identifier, each variable receives the current utility value.
+@mixin generate-variables($utility, $value) {
+  @if map.has-key($utility, variables) {
+    $variables: map.get($utility, variables);
+    @if meta.type-of($variables) == "map" {
+      @each $var-key, $var-value in $variables {
+        --#{$var-key}: #{$var-value};
+      }
+    } @else {
+      // Treat as a list (or single identifier) — each variable gets the utility value
+      @each $var-name in $variables {
+        --#{$var-name}: #{$value};
+      }
+    }
+  }
+}
+
 // Helper mixin to generate CSS properties for both legacy and property map approaches
-@mixin generate-properties($utility, $propertyMap, $properties, $value) {
-  @if $propertyMap != null {
-    // New Property-Value Mapping approach
-    @each $property, $defaultValue in $propertyMap {
-      // If value is a map, check if it has a key for this property
-      // Otherwise, use defaultValue (or $value if defaultValue is null)
-      $actualValue: $defaultValue;
+@mixin generate-properties($utility, $property-map, $properties, $value) {
+  @if $property-map != null {
+    // Property-Value Mapping approach
+    @each $property, $default-value in $property-map {
+      // If value is a map, check if it has a key for this property.
+      // Otherwise, use default-value (or $value if default-value is null).
+      $actual-value: $default-value;
       @if meta.type-of($value) == "map" and map.has-key($value, $property) {
-        $actualValue: map.get($value, $property);
-      } @else if $defaultValue == null {
-        $actualValue: $value;
+        $actual-value: map.get($value, $property);
+      } @else if $default-value == null {
+        $actual-value: $value;
       }
       @if map.get($utility, important) {
-        #{$property}: $actualValue !important; // stylelint-disable-line declaration-no-important
+        #{$property}: $actual-value !important; // stylelint-disable-line declaration-no-important
       } @else {
-        #{$property}: $actualValue;
+        #{$property}: $actual-value;
       }
     }
   } @else {
   }
 
   // Warn on unknown keys (likely typos)
-  $valid-keys: property, values, class, selector, responsive, print, important, state, variables, child-selector;
+  $valid-keys: property, values, class, selector, responsive, print, dark, important, state, variables, child-selector, enabled;
   @each $key in map.keys($utility) {
     @if not list.index($valid-keys, $key) {
       @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}";
     }
   }
 
+  // Validate boolean keys
+  @each $bool-key in (responsive, print, dark, important, enabled) {
+    @if map.has-key($utility, $bool-key) {
+      $val: map.get($utility, $bool-key);
+      @if $val != true and $val != false {
+        @error "Utility key `#{$bool-key}` should be a boolean (true or false), got: #{$val}";
+      }
+    }
+  }
+
   // Determine if we're generating a class, or an attribute selector
-  $selectorType: "class";
+  $selector-type: "class";
   @if map.has-key($utility, selector) {
-    $selectorType: map.get($utility, selector);
+    $selector-type: map.get($utility, selector);
     // Validate selector type
     $valid-selectors: "class", "attr-starts", "attr-includes";
-    @if not list.index($valid-selectors, $selectorType) {
-      @error "Invalid `selector` value `#{$selectorType}`. Must be one of: #{$valid-selectors}";
+    @if not list.index($valid-selectors, $selector-type) {
+      @error "Invalid `selector` value `#{$selector-type}`. Must be one of: #{$valid-selectors}";
     }
   }
-  // Then get the class name to use in a class (e.g., .class) or in a attribute selector (e.g., [class^="class"])
-  $selectorClass: map.get($utility, class);
+  // Then get the class name to use in a class (e.g., .class) or in an attribute selector (e.g., [class^="class"])
+  $selector-class: map.get($utility, class);
 
   // Attribute selectors require a `class` key
-  @if $selectorType != "class" and not map.has-key($utility, class) {
-    @error "Utility with `selector: #{$selectorType}` requires a `class` key.";
+  @if $selector-type != "class" and not map.has-key($utility, class) {
+    @error "Utility with `selector: #{$selector-type}` requires a `class` key.";
   }
 
   // Get the list or map of values and ensure it's a map
 
   @each $key, $value in $values {
     $properties: map.get($utility, property);
-    $propertyMap: null;
-    $customClass: "";
+    $property-map: null;
+    $custom-class: "";
 
-    // Check if property is a map (new Property-Value Mapping approach)
+    // Check if property is a map (Property-Value Mapping approach)
     @if meta.type-of($properties) == "map" {
-      $propertyMap: $properties;
-      $customClass: "";
+      $property-map: $properties;
       @if map.has-key($utility, class) {
-        $customClass: map.get($utility, class);
+        $custom-class: map.get($utility, class);
       }
     } @else {
-      // Legacy approach: Multiple properties are possible, for example with vertical or horizontal margins or paddings
+      // Legacy approach: multiple properties are possible, for example with vertical or horizontal margins or paddings
       @if meta.type-of($properties) == "string" {
         $properties: list.append((), $properties);
       }
       // Use custom class if present, otherwise use the first value from the list of properties
       @if map.has-key($utility, class) {
-        $customClass: map.get($utility, class);
+        $custom-class: map.get($utility, class);
       } @else {
-        $customClass: list.nth($properties, 1);
+        $custom-class: list.nth($properties, 1);
       }
-      @if $customClass == null {
-        $customClass: "";
+      @if $custom-class == null {
+        $custom-class: "";
       }
     }
 
     }
 
     // Don't add a dash before value key if value key is null (e.g. with shadow class)
-    $customClassModifier: "";
+    $custom-class-modifier: "";
     @if $key {
-      @if $customClass == "" {
-        $customClassModifier: $key;
+      @if $custom-class == "" {
+        $custom-class-modifier: $key;
       } @else {
-        $customClassModifier: "-" + $key;
+        $custom-class-modifier: "-" + $key;
       }
     }
 
     // Build the class name fragment (without prefix or dot) for reuse in state variants
-    $className: "";
-    @if $selectorType == "class" {
-      @if $customClass != "" {
-        $className: $customClass + $customClassModifier;
-      } @else if $selectorClass != null and $selectorClass != "" {
-        $className: $selectorClass + $customClassModifier;
+    $class-name: "";
+    @if $selector-type == "class" {
+      @if $custom-class != "" {
+        $class-name: $custom-class + $custom-class-modifier;
+      } @else if $selector-class != null and $selector-class != "" {
+        $class-name: $selector-class + $custom-class-modifier;
       } @else {
-        $className: $customClassModifier;
+        $class-name: $custom-class-modifier;
       }
     }
 
     $selector: "";
-    @if $selectorType == "class" {
-      $selector: ".#{$prefix + $className}";
-    } @else if $selectorType == "attr-starts" {
-      $selector: "[class^=\"#{$selectorClass}\"]";
-    } @else if $selectorType == "attr-includes" {
-      $selector: "[class*=\"#{$selectorClass}\"]";
+    @if $selector-type == "class" {
+      $selector: ".#{$prefix + $class-name}";
+    } @else if $selector-type == "attr-starts" {
+      $selector: "[class^=\"#{$selector-class}\"]";
+    } @else if $selector-type == "attr-includes" {
+      $selector: "[class*=\"#{$selector-class}\"]";
     }
 
-    // @debug $utility;
-    // @debug $selectorType;
-    // @debug $selector;
-    // @debug $properties;
-    // @debug $values;
-
     // Apply child-selector wrapping if present (wraps in :where() for zero specificity)
     $child-sel: null;
     @if map.has-key($utility, child-selector) {
     }
 
     #{$final-selector} {
-      // Generate CSS custom properties (variables) if provided
-      // Variables receive the current utility value, then properties reference them
-      @if map.has-key($utility, variables) {
-        $variables: map.get($utility, variables);
-        @if meta.type-of($variables) == "list" {
-          // If variables is a list, each variable gets the utility value
-          @each $var-name in $variables {
-            --#{$var-name}: #{$value};
-          }
-        } @else if meta.type-of($variables) == "map" {
-          // If variables is a map, use the provided values (for static variables)
-          @each $var-key, $var-value in $variables {
-            --#{$var-key}: #{$var-value};
-          }
-        }
-      }
-      @include generate-properties($utility, $propertyMap, $properties, $value);
+      @include generate-variables($utility, $value);
+      @include generate-properties($utility, $property-map, $properties, $value);
     }
 
     // Generate state variants (e.g., hover:link-10 instead of link-10-hover)
     @if $state != () {
       @each $state-variant in $state {
-        $state-selector: ".#{$prefix}#{$state-variant}\\:#{$className}:#{$state-variant}";
+        $state-selector: ".#{$prefix}#{$state-variant}\\:#{$class-name}:#{$state-variant}";
         @if $child-sel {
           $state-selector: ":where(#{$state-selector} #{$child-sel})";
         }
 
         #{$state-selector} {
-          // Generate CSS custom properties (variables) if provided
-          @if map.has-key($utility, variables) {
-            $variables: map.get($utility, variables);
-            @if meta.type-of($variables) == "list" {
-              // If variables is a list, each variable gets the utility value
-              @each $var-name in $variables {
-                --#{$var-name}: #{$value};
-              }
-            } @else if meta.type-of($variables) == "map" {
-              // If variables is a map, use the provided values (for static variables)
-              @each $var-key, $var-value in $variables {
-                --#{$var-key}: #{$var-value};
-              }
-            }
-          }
-          @include generate-properties($utility, $propertyMap, $properties, $value);
+          @include generate-variables($utility, $value);
+          @include generate-properties($utility, $property-map, $properties, $value);
         }
       }
     }
   }
 }
+
+// Generates all utility classes: base, responsive, print, and dark.
+// Extracted so that tests can call this mixin directly with a custom $utilities map
+// rather than having to mirror the loop conditions inline.
+@mixin generate-utilities-loop($utilities, $breakpoints) {
+  // Base + responsive (one pass per breakpoint)
+  @each $breakpoint in map.keys($breakpoints) {
+    @include bp.media-breakpoint-up($breakpoint, $breakpoints) {
+      $prefix: bp.breakpoint-prefix($breakpoint, $breakpoints);
+
+      @each $key, $utility in $utilities {
+        @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and (map.get($utility, responsive) or $prefix == "") {
+          @include generate-utility($utility, $prefix);
+        }
+      }
+    }
+  }
+
+  // Print utilities
+  @media print {
+    @each $key, $utility in $utilities {
+      @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, print) == true {
+        @include generate-utility($utility, "print\\:");
+      }
+    }
+  }
+
+  // Dark utilities
+  @media (prefers-color-scheme: dark) {
+    @each $key, $utility in $utilities {
+      @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, dark) == true {
+        @include generate-utility($utility, "dark\\:");
+      }
+    }
+  }
+}
index edfdf2906a66231fd4ed4a502f92ebadda4f3c33..cdddaa25d17b703f382f973e11d836236a66e419 100644 (file)
@@ -8,7 +8,7 @@ module.exports = {
   spec_dir: 'scss',
   // Make Jasmine look for `.test.scss` files
   // spec_files: ['**/*.{test,spec}.scss'],
-  spec_files: ['**/_utilities.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'],
+  spec_files: ['**/_utilities.test.scss', '**/utilities/_api.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'],
   // Compile them into JS scripts running `sass-true`
   requires: [path.join(__dirname, 'sass-true/register.cjs')],
   // Ensure we use `require` so that the require.extensions works
index bfcfcbbee59bd006f2519c654cdce7fd2a886dd4..83d8f3ab27078613c8e0a1a155c9d503d0757314 100644 (file)
@@ -382,4 +382,213 @@ $true-terminal-output: false;
       }
     }
   }
+
+  @include describe("selector") {
+    @include it("generates an attr-starts selector") {
+      @include test-generate-utility(
+        (
+          selector: "attr-starts",
+          class: "ratio-",
+          property: aspect-ratio,
+          values: var(--ratio),
+        )
+      ) {
+        [class^="ratio-"] {
+          aspect-ratio: var(--ratio);
+        }
+      }
+    }
+
+    @include it("generates an attr-includes selector") {
+      @include test-generate-utility(
+        (
+          selector: "attr-includes",
+          class: "ratio-",
+          property: aspect-ratio,
+          values: var(--ratio),
+        )
+      ) {
+        [class*="ratio-"] {
+          aspect-ratio: var(--ratio);
+        }
+      }
+    }
+  }
+
+  @include describe("child-selector") {
+    @include it("wraps the selector in :where() with the child selector appended") {
+      @include test-generate-utility(
+        (
+          property: margin-inline-end,
+          class: space-x,
+          child-selector: "> :not(:last-child)",
+          values: (1: .25rem, 2: .5rem)
+        )
+      ) {
+        :where(.space-x-1 > :not(:last-child)) {
+          margin-inline-end: .25rem;
+        }
+
+        :where(.space-x-2 > :not(:last-child)) {
+          margin-inline-end: .5rem;
+        }
+      }
+    }
+
+    @include it("wraps state variant selectors in :where() as well") {
+      @include test-generate-utility(
+        (
+          property: margin-inline-end,
+          class: space-x,
+          child-selector: "> :not(:last-child)",
+          state: hover,
+          values: (1: .25rem)
+        )
+      ) {
+        :where(.space-x-1 > :not(:last-child)) {
+          margin-inline-end: .25rem;
+        }
+
+        :where(.hover\:space-x-1:hover > :not(:last-child)) {
+          margin-inline-end: .25rem;
+        }
+      }
+    }
+  }
+
+  @include describe("variables") {
+    @include it("emits each list variable with the utility value") {
+      @include test-generate-utility(
+        (
+          property: color,
+          class: link,
+          variables: link-color,
+          values: (10: 10%, 50: 50%)
+        )
+      ) {
+        .link-10 {
+          --link-color: 10%;
+          color: 10%;
+        }
+
+        .link-50 {
+          --link-color: 50%;
+          color: 50%;
+        }
+      }
+    }
+
+    @include it("emits each map variable with its static value") {
+      @include test-generate-utility(
+        (
+          property: text-decoration-color,
+          class: link-underline,
+          variables: ("link-underline-opacity": 1),
+          values: (primary: #06c, danger: #c00)
+        )
+      ) {
+        .link-underline-primary {
+          --link-underline-opacity: 1;
+          text-decoration-color: #06c;
+        }
+
+        .link-underline-danger {
+          --link-underline-opacity: 1;
+          text-decoration-color: #c00;
+        }
+      }
+    }
+
+    @include it("emits variables in state variant blocks too") {
+      @include test-generate-utility(
+        (
+          property: color,
+          class: link,
+          variables: link-color,
+          state: hover,
+          values: (10: 10%)
+        )
+      ) {
+        .link-10 {
+          --link-color: 10%;
+          color: 10%;
+        }
+
+        .hover\:link-10:hover {
+          --link-color: 10%;
+          color: 10%;
+        }
+      }
+    }
+  }
+
+  @include describe("property map") {
+    @include it("uses null default to pass the utility value to a property") {
+      @include test-generate-utility(
+        (
+          property: (
+            "--bg": null,
+            "background-color": var(--bg)
+          ),
+          class: bg,
+          values: (primary: #06c, danger: #c00)
+        )
+      ) {
+        .bg-primary {
+          --bg: #06c;
+          background-color: var(--bg);
+        }
+
+        .bg-danger {
+          --bg: #c00;
+          background-color: var(--bg);
+        }
+      }
+    }
+
+    @include it("uses a per-value map to override individual properties") {
+      @include test-generate-utility(
+        (
+          property: (
+            "font-size": 1rem,
+            "line-height": 1.5
+          ),
+          class: text,
+          values: (
+            "sm": ("font-size": .875rem, "line-height": 1.25),
+            "lg": ("font-size": 1.25rem, "line-height": 1.75),
+          )
+        )
+      ) {
+        .text-sm {
+          font-size: .875rem;
+          line-height: 1.25;
+        }
+
+        .text-lg {
+          font-size: 1.25rem;
+          line-height: 1.75;
+        }
+      }
+    }
+
+    @include it("applies !important to all mapped properties") {
+      @include test-generate-utility(
+        (
+          property: (
+            "--bg": null,
+            "background-color": var(--bg)
+          ),
+          class: bg,
+          important: true,
+          values: (primary: #06c)
+        )
+      ) {
+        .bg-primary {
+          --bg: #06c !important;
+          background-color: var(--bg) !important;
+        }
+      }
+    }
+  }
 }
index 00dbc4ee9ff17defc35e335526217161111fa128..e40ee9dc3bd97fc131556e0cc796eefa4a423ff3 100644 (file)
 // check-unused-imports-disable — test infrastructure imports.
-@use "../../functions";
-@use "../../config";
-@use "../../maps";
-@use "../../mixins";
-
-$utilities: ();
-
-@include describe("utilities/api") {
-  @include it("generates utilities for each breakpoints") {
-    $utilities: (
-      margin: (
-        property: margin,
-        important: true,
-        values: auto
-      ),
-      padding: (
-        property: padding,
-        responsive: true,
-        important: true,
-        values: 1rem
-      ),
-      font-size: (
-        property: font-size,
-        values: (large: 1.25rem),
-        important: true,
-        print: true
-      )
-    ) !global;
-
-    $breakpoints: (
-      xs: 0,
-      sm: 333px,
-      md: 666px
-    ) !global;
-
-    @include assert() {
-      @include output() {
-        @use "../../utilities/api";
-      }
+@use "sass:map";
+@use "../../config" as *;
+@use "../../functions" as *;
+@use "../../mixins/utilities" as *;
+
+// Integration-style tests for the utilities API loop.
+// Each test calls generate-utilities-loop — the same mixin that _api.scss calls — so
+// a logic change in the real loop is immediately caught here without any mirrored conditions.
+//
+// A minimal two-breakpoint map is used to keep output predictable and tests fast.
+$test-breakpoints: (
+  xs: 0,
+  sm: 576px,
+);
+
+@include describe("utilities/api loop conditions") {
+  @include describe("enabled") {
+    @include it("skips utilities with enabled: false") {
+      $test-utilities: (
+        opacity: (
+          property: opacity,
+          values: (50: .5)
+        ),
+        float: (
+          property: float,
+          enabled: false,
+          values: (start: inline-start, end: inline-end)
+        )
+      );
 
-      @include expect() {
-        // margin is not set to responsive
-        .margin-auto {
-          margin: auto !important;
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
         }
 
-        // padding is, though
-        .padding-1rem {
-          padding: 1rem !important;
+        @include expect() {
+          .opacity-50 {
+            opacity: .5;
+          }
         }
+      }
+    }
 
-        .font-size-large {
-          font-size: 1.25rem !important;
+    @include it("outputs utilities without an explicit enabled key") {
+      $test-utilities: (
+        opacity: (
+          property: opacity,
+          values: (25: .25)
+        )
+      );
+
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
         }
 
-        @media (width >= 333px) {
-          .sm\:padding-1rem {
-            padding: 1rem !important;
+        @include expect() {
+          .opacity-25 {
+            opacity: .25;
           }
         }
+      }
+    }
+  }
 
-        @media (width >= 666px) {
-          .md\:padding-1rem {
-            padding: 1rem !important;
-          }
+  @include describe("responsive") {
+    @include it("generates responsive classes only for utilities with responsive: true") {
+      $test-utilities: (
+        opacity: (
+          property: opacity,
+          responsive: true,
+          values: (50: .5)
+        ),
+        float: (
+          property: float,
+          values: (start: inline-start)
+        )
+      );
+
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
         }
 
-        @media print {
-          .print\:font-size-large {
-            font-size: 1.25rem !important;
+        @include expect() {
+          .opacity-50 {
+            opacity: .5;
+          }
+
+          .float-start {
+            float: inline-start;
+          }
+
+          @media (width >= 576px) {
+            .sm\:opacity-50 {
+              opacity: .5;
+            }
           }
         }
       }
-
     }
-  }
 
-  @include it("generates utilities without !important when important: false or not set") {
-    $utilities: (
-      opacity: (
-        property: opacity,
-        values: (
-          0: 0,
-          50: .5,
-          100: 1
+    @include it("does not generate responsive classes for utilities without responsive: true") {
+      $test-utilities: (
+        float: (
+          property: float,
+          values: (start: inline-start)
         )
-      ),
-      text-align: (
-        property: text-align,
-        important: false,
-        values: (
-          start: left,
-          center: center
-        )
-      )
-    ) !global;
+      );
 
-    @include assert() {
-      @include output() {
-        @import "../../utilities/api";
-      }
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
+        }
 
-      @include expect() {
-        .opacity-0 {
-          opacity: 0;
+        @include expect() {
+          .float-start {
+            float: inline-start;
+          }
+          // nothing at sm — responsive is not set
         }
+      }
+    }
+  }
 
-        .opacity-50 {
-          opacity: .5;
+  @include describe("dark") {
+    @include it("generates dark-prefixed classes for utilities with dark: true") {
+      $test-utilities: (
+        opacity: (
+          property: opacity,
+          values: (50: .5)
+        ),
+        bg: (
+          property: background-color,
+          class: bg,
+          dark: true,
+          values: (white: #fff, black: #000)
+        )
+      );
+
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
         }
 
-        .opacity-100 {
-          opacity: 1;
+        @include expect() {
+          .opacity-50 {
+            opacity: .5;
+          }
+
+          .bg-white {
+            background-color: #fff;
+          }
+
+          .bg-black {
+            background-color: #000;
+          }
+
+          @media (prefers-color-scheme: dark) {
+            .dark\:bg-white {
+              background-color: #fff;
+            }
+
+            .dark\:bg-black {
+              background-color: #000;
+            }
+          }
         }
+      }
+    }
 
-        .text-align-start {
-          text-align: left;
+    @include it("does not generate dark classes for utilities without dark: true") {
+      $test-utilities: (
+        opacity: (
+          property: opacity,
+          values: (50: .5)
+        )
+      );
+
+      @include assert() {
+        @include output() {
+          @include generate-utilities-loop($test-utilities, $test-breakpoints);
         }
 
-        .text-align-center {
-          text-align: center;
+        @include expect() {
+          .opacity-50 {
+            opacity: .5;
+          }
+          // nothing in @media (prefers-color-scheme: dark) — dark is not set
         }
       }
     }
index 346eb223315afb4a329dcf59fb5f97bbc4f859dc..327573a398ab630c4a34483f862c5738915bda10 100644 (file)
@@ -1,37 +1,7 @@
-@use "sass:map";
-@use "sass:meta";
 @use "../config" as *;
-@use "../layout/breakpoints" as *;
 @use "../mixins/utilities" as *;
 @use "../utilities" as *;
 
 @layer utilities {
-  // Loop over each breakpoint
-  @each $breakpoint in map.keys($breakpoints) {
-
-    // Generate media query if needed
-    @include media-breakpoint-up($breakpoint) {
-      $prefix: breakpoint-prefix($breakpoint, $breakpoints);
-
-      // Loop over each utility property
-      @each $key, $utility in $utilities {
-        // The utility can be disabled with `false`, thus check if the utility is a map first
-        // Only proceed if responsive media queries are enabled or if it's the base media query
-        @if meta.type-of($utility) == "map" and (map.get($utility, responsive) or $prefix == "") {
-          @include generate-utility($utility, $prefix);
-        }
-      }
-    }
-  }
-
-  // Print utilities
-  @media print {
-    @each $key, $utility in $utilities {
-      // The utility can be disabled with `false`, thus check if the utility is a map first
-      // Then check if the utility needs print styles
-      @if meta.type-of($utility) == "map" and map.get($utility, print) == true {
-        @include generate-utility($utility, "print\\:");
-      }
-    }
-  }
+  @include generate-utilities-loop($utilities, $breakpoints);
 }
index 46cf4fcf549c91cdc6ff8779db2603333ce9e7ca..1e7971e31db43ff5b1d1410e743e953abf941be2 100644 (file)
@@ -39,7 +39,7 @@ const deps = frontmatter.deps ?? []
 const depsCount = deps.length
 ---
 
-<div class="bd-page-meta w-100 d-grid md:grid-cols-2 lg:d-flex gap-5 row-gap-2 px-4 py-3 bg-1 fg-2 fs-sm rounded-3">
+<div class="bd-page-meta w-100 d-grid md:grid-cols-2 lg:d-flex gap-5 row-gap-2 px-4 py-3 bg-1 fg-2 fs-sm rounded-5">
   {
     frontmatter.js === 'required' && (
       <div class="d-flex align-items-center gap-2">
index 4e135799543272799e4cf7dfb30dd69788d60f7f..bfb6bd6c62457b2fe17015402d4f232874d3794f 100644 (file)
@@ -13,7 +13,7 @@ Bootstrap 6 brings with it a new philosophy of using Sass and CSS together to bu
 
 Previous major versions have treated Sass as both programmatic and visual customization with CSS variables adding a more complicated, and less complete, secondary layer. This has been greatly improved in v6.
 
-On top of that, we use [PostCSS](https://postcss.org/) to prefix our CSS variables with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate.
+On top of that, we use [PostCSS](https://postcss.org/) to [prefix our CSS variables](https://github.com/twbs/postcss-prefix-custom-properties) with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate.
 
 ### Sass
 
index 4c6768967f916ea1fa8d22fcbd76c914825f3c26..c84ffe460403031ee9402374e7ab0496bf32888f 100644 (file)
@@ -15,7 +15,7 @@ The `$utilities` map contains all our utilities and is later merged with your cu
 <BsTable class="table table-utilities">
 | Option | Type | Default&nbsp;value | Description |
 | --- | --- | --- | --- |
-| [`property`](#property) | **Required** | – | Name of the property, this can be a string or an array of strings (e.g., horizontal paddings or margins). |
+| [`property`](#property) | **Required** | – | Name of the property. Can be a string, a space-separated list of strings (e.g., horizontal paddings or margins), or a **map** of property-to-default-value pairs (see [Property-Value Mapping](#property-value-mapping)). |
 | [`values`](#values) | **Required** | – | List of values, or a map if you don’t want the class name to be the same as the value. If `null` is used as map key, `class` is not prepended to the class name. |
 | [`selector`](#selector) | Optional | `class` | Type of CSS selector in the generated CSS ruleset. Can be `class`, `attr-starts`, or `attr-includes`. |
 | [`child-selector`](#child-selector) | Optional | null | A child/descendant selector appended to the utility’s selector, wrapped in `:where()` for zero specificity. Use to target children instead of the element itself. |
@@ -25,6 +25,8 @@ The `$utilities` map contains all our utilities and is later merged with your cu
 | [`responsive`](#responsive) | Optional | `false` | Boolean indicating if responsive classes should be generated. |
 | [`important`](#importance) | Optional | `false` | Boolean indicating if `!important` should be added to the utility’s CSS rules. |
 | [`print`](#print) | Optional | `false` | Boolean indicating if print classes need to be generated. |
+| [`dark`](#dark) | Optional | `false` | Boolean indicating if `dark:` prefixed classes should be generated, scoped to `@media (prefers-color-scheme: dark)`. |
+| [`enabled`](#enabled) | Optional | `true` | Set to `false` to suppress output for a utility while keeping its definition in the `$utilities` map. |
 </BsTable>
 
 ## API explained
@@ -58,7 +60,7 @@ Which outputs the following:
 
 ### `property`
 
-<div class="badge fs-6 bg-accent mb-3">Required</div>
+<div class="badge badge-subtle theme-danger fs-sm fw-normal align-self-start">Required</div>
 
 The `property` key must be set for any utility, and it must contain a valid CSS property. This property is used in the generated utility’s ruleset. When the `class` key is omitted, it also serves as the default class name. Consider the `text-decoration` utility:
 
@@ -79,9 +81,69 @@ Output:
 .text-decoration-line-through { text-decoration: line-through; }
 ```
 
+#### Property value mapping
+
+The `property` key also accepts a **map** of CSS property names to default values. This lets a single utility class set multiple properties at once, and lets values override individual properties when the value is itself a map.
+
+When a property's default value in the map is `null`, the utility value is used directly for that property. When a default value is set, it is used unless the utility's value map contains a matching key.
+
+A common pattern is pairing a CSS custom property (which receives the utility value) with a regular property that references it:
+
+```scss
+$utilities: (
+  "bg-color": (
+    property: (
+      "--bg": null,
+      "background-color": var(--bg)
+    ),
+    class: bg,
+    values: (
+      primary: var(--blue-500),
+      danger:  var(--red-500),
+    )
+  )
+);
+```
+
+Output:
+
+```css
+.bg-primary { --bs-bg: var(--bs-blue-500); background-color: var(--bs-bg); }
+.bg-danger  { --bs-bg: var(--bs-red-500);  background-color: var(--bs-bg); }
+```
+
+<Callout type="info">
+  Source CSS variables are used without a `--bs-` prefix, which is added in our compiled CSS with [our PostCSS plugin](https://github.com/twbs/postcss-prefix-custom-properties).
+</Callout>
+
+You can also set per-value overrides. When `values` contains a nested map whose keys match properties in the `property` map, those values are used for the matching property:
+
+```scss
+$utilities: (
+  "text-size": (
+    property: (
+      "font-size":   1rem,
+      "line-height": 1.5
+    ),
+    class: text,
+    values: (
+      "sm": ("font-size": .875rem, "line-height": 1.25),
+      "lg": ("font-size": 1.25rem, "line-height": 1.75),
+    )
+  )
+);
+```
+
+Output:
+
+```css
+.text-sm { font-size: .875rem; line-height: 1.25; }
+.text-lg { font-size: 1.25rem; line-height: 1.75; }
+```
+
 ### `values`
 
-<div class="badge fs-6 bg-accent mb-3">Required</div>
+<div class="badge badge-subtle theme-danger fs-sm fw-normal align-self-start">Required</div>
 
 Use the `values` key to specify which values for the specified `property` should be used in the generated class names and rules. Can be a list or map (set in the utilities or in a Sass variable).
 
@@ -293,7 +355,7 @@ $utilities: (
   "link-opacity": (
     property: color,
     class: link,
-    variables: (link-color),
+    variables: link-color,
     values: (
       10: 10%,
       50: 50%,
@@ -476,6 +538,52 @@ Output:
 }
 ```
 
+### Dark
+
+Enabling the `dark` option generates `dark:` prefixed classes scoped to `@media (prefers-color-scheme: dark)`. This follows the same prefix convention as responsive and print utilities.
+
+```scss
+$utilities: (
+  "bg-color": (
+    property: background-color,
+    class: bg,
+    dark: true,
+    values: (
+      white: #fff,
+      black: #000,
+    )
+  )
+);
+```
+
+Output:
+
+```css
+.bg-white { background-color: #fff; }
+.bg-black { background-color: #000; }
+
+@media (prefers-color-scheme: dark) {
+  .dark\:bg-white { background-color: #fff; }
+  .dark\:bg-black { background-color: #000; }
+}
+```
+
+### Enabled
+
+Set `enabled: false` to suppress output for a utility without removing it from the `$utilities` map. This is useful when you want to keep the utility's definition available for reference or downstream `map.get()` calls, but don't want it to emit any CSS.
+
+```scss
+$utilities: map.merge(
+  $utilities,
+  (
+    "float": map.merge(
+      map.get($utilities, "float"),
+      ( enabled: false ),
+    ),
+  )
+);
+```
+
 ## Importance
 
 Utilities generated by the API no longer include `!important` by default in v6. This is because we now use CSS layers to ensure utilities override components and modifier classes as intended. You can enable `!important` on a per-utility basis by setting the `important` option to `true`.
@@ -510,30 +618,40 @@ This will generate utilities with `!important`:
 
 Now that you’re familiar with how the utilities API works, learn how to add your own custom classes and modify our default utilities.
 
+The examples below use two different import styles depending on the use case:
+
+- **`@use "bootstrap" with (...)`**—Use this when compiling the full Bootstrap framework from a single entrypoint. Configuration is passed via `with (...)` and Bootstrap handles merging in one step.
+- **`@use "bootstrap/scss/utilities" as *` + `@use "bootstrap/scss/utilities/api"`**—Use this for focused, utility-only builds where you need to read from or merge the existing `$utilities` map before passing it to the API layer. Using `as *` avoids a namespace prefix so you can reference `$utilities` directly.
+
 ### Override utilities
 
-Override existing utilities by using the same key. For example, if you want additional responsive overflow utility classes, you can do this:
+Override existing utilities by using the same key and passing the new definition via the `$utilities` configuration variable when loading Bootstrap. For example, take our existing overflow utility class definition:
+
+<ScssDocs name="utils-overflow" file="scss/_utilities.scss" />
+
+If you want to make those responsive, and adjust the values used, you can do this:
 
 ```scss
-$utilities: (
-  "overflow": (
-    responsive: true,
-    property: overflow,
-    values: visible hidden scroll auto,
-  ),
+// my-bootstrap.scss
+@use "bootstrap" with (
+  $utilities: (
+    "overflow": (
+      responsive: true,
+      // property: overflow,
+      values: visible hidden scroll auto,
+    ),
+  )
 );
 ```
 
 ### Add utilities
 
-New utilities can be added to the default `$utilities` map with a `map.merge`. Make sure our required Sass files and `_utilities.scss` are loaded first, then use `map.merge` to add your additional utilities. For example, here’s how to add a responsive `cursor` utility with three values.
+New utilities can be added to the default `$utilities` map by passing them via the `$utilities` configuration variable when loading Bootstrap. Bootstrap merges your map on top of its defaults, so only supply the new utilities you want to add.
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
-
-$utilities: map.merge(
-  $utilities,
-  (
+// my-bootstrap.scss
+@use "bootstrap" with (
+  $utilities: (
     "cursor": (
       property: cursor,
       class: cursor,
@@ -542,16 +660,16 @@ $utilities: map.merge(
     )
   )
 );
-
-@use "bootstrap/scss/utilities/api";
 ```
 
 ### Modify utilities
 
-Modify existing utilities in the default `$utilities` map with `map.get` and `map.merge` functions. In the example below, we’re adding an additional value to the `width` utilities. Start with an initial `map.merge` and then specify which utility you want to modify. From there, fetch the nested `"width"` map with `map.get` to access and modify the utility’s options and values.
+Modify existing utilities by passing a modified copy of the utility via the `$utilities` configuration variable. Use `map.get` and `map.merge` to build the new value from the existing definition. In the example below, we’re adding an additional value to the `width` utilities:
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+// my-bootstrap.scss
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.merge(
   $utilities,
@@ -568,7 +686,7 @@ $utilities: map.merge(
   )
 );
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
 
 #### Enable responsive
@@ -576,7 +694,8 @@ $utilities: map.merge(
 You can enable responsive classes for an existing set of utilities that are not currently responsive by default. For example, to make the `border` classes responsive:
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.merge(
   $utilities,
@@ -588,7 +707,7 @@ $utilities: map.merge(
   )
 );
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
 
 This will now generate responsive variations of `.border` and `.border-0` for each breakpoint. Your generated CSS will look like this:
@@ -628,7 +747,8 @@ This will now generate responsive variations of `.border` and `.border-0` for ea
 Missing v4 utilities, or used to another naming convention? The utilities API can be used to override the resulting `class` of a given utility—for example, to rename `.ms-*` utilities to oldish `.ml-*`:
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.merge(
   $utilities,
@@ -640,7 +760,7 @@ $utilities: map.merge(
   )
 );
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
 
 ### Remove utilities
@@ -648,17 +768,19 @@ $utilities: map.merge(
 Remove any of the default utilities with the [`map.remove()` Sass function](https://sass-lang.com/documentation/modules/map/#remove).
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.remove($utilities, "width", "float");
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
 
 You can also use the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge) and set the group key to `null` to remove the utility.
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.merge(
   $utilities,
@@ -667,7 +789,7 @@ $utilities: map.merge(
   )
 );
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
 
 ### Add, remove, modify
@@ -675,7 +797,8 @@ $utilities: map.merge(
 You can add, remove, and modify many utilities all at once with the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge). Here’s how you can combine the previous examples into one larger map.
 
 ```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
 
 $utilities: map.merge(
   $utilities,
@@ -697,5 +820,5 @@ $utilities: map.merge(
   )
 );
 
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
 ```
index bbcb41098ab753cd4115df0f3465932ff1007089..5e3b36373f0b4bfc185d62b5efbb8de4492a00c8 100644 (file)
@@ -42,6 +42,20 @@ Use the scaling classes for larger or smaller rounded corners. Sizes range from
 <Placeholder width="75" height="75" class="rounded-start-pill" title="Example left rounded pill image" />
 <Placeholder width="75" height="75" class="rounded-5 rounded-top-0" title="Example extra large bottom rounded image" />`} />
 
+## Mixing
+
+Use `.rounded-size-{n}` to set the radius size without rounding all corners. Combine it with side classes like `.rounded-top` or `.rounded-end` to round only specific sides at that size. Because these use CSS variables for sizing, you may need to apply additional classes to nested elements to prevent inheritance.
+
+<Example class="bd-example-rounded-utils" code={`<Placeholder width="75" height="75" class="rounded-size-6 rounded-top" title="Top corners only, size 6" />
+<Placeholder width="75" height="75" class="rounded-size-6 rounded-top rounded-end" title="Top and end corners, size 6" />
+<Placeholder width="75" height="75" class="rounded-size-3 rounded-top rounded-bottom" title="Top and bottom, size 3" />
+<Placeholder width="75" height="75" class="rounded-size-pill rounded-top" title="Top corners only, pill" />`} />
+
+When `.rounded-{n}` is used alongside a side class, the number sets the `--rounded-size` variable that the side class inherits.
+
+<Example class="bd-example-rounded-utils" code={`<Placeholder width="75" height="75" class="rounded-6 rounded-bottom-0" title="All corners size 6, bottom corners removed" />
+<Placeholder width="75" height="75" class="rounded-3 rounded-top" title="All corners size 3, top corners stay size 3 via --rounded-size" />`} />
+
 ## CSS
 
 ### Variables
@@ -50,6 +64,8 @@ Radius tokens are generated from the `$spacers` map in `scss/_root.scss`. [Learn
 
 <ScssDocs name="root-spacer-loop" file="scss/_root.scss" />
 
+The `--rounded-size` local CSS variable is set by `.rounded-{n}` and `.rounded-size-{n}` and consumed by the side utilities (`.rounded-top`, `.rounded-end`, `.rounded-bottom`, `.rounded-start`) as their default radius. You can also set it directly in your own CSS or via an inline style to control the side utilities from a single point.
+
 ### Sass maps
 
 ### Sass mixins
index e64ad49058d41cd2e8120fc22a643255aa57f2a9..a3cca84464157bcd37d79bd5f1ffa2a1e053366b 100644 (file)
@@ -40,18 +40,6 @@ Prevent long strings of text from breaking your components' layout by using `.te
 
 Text wrapping utilities are declared in our utilities API in `scss/_utilities.scss`. [Learn how to use the utilities API.]([[docsref:/utilities/api#using-the-api]])
 
-```scss
-"white-space": (
-  property: white-space,
-  class: text,
-  values: (
-    wrap: normal,
-    nowrap: nowrap,
-  )
-),
-"word-wrap": (
-  property: word-wrap word-break,
-  class: text,
-  values: (break: break-word),
-),
-```
+<ScssDocs name="utils-text-wrap" file="scss/_utilities.scss" />
+
+<ScssDocs name="utils-text-break" file="scss/_utilities.scss" />