]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
v6: Prevent `border-color` utility inheritance, harden other utilities (#42669)
authorMark Otto <markd.otto@gmail.com>
Tue, 14 Jul 2026 16:48:48 +0000 (09:48 -0700)
committerGitHub <noreply@github.com>
Tue, 14 Jul 2026 16:48:48 +0000 (09:48 -0700)
* Update utility API to prevent border-color inheritance, write custom @property rules

* fix fg var

scss/_accordion.scss
scss/_utilities.scss
scss/mixins/_utilities.scss
scss/utilities/_api.scss
site/src/content/docs/utilities/border-color.mdx
site/src/content/docs/utilities/border.mdx
site/src/content/docs/utilities/divide.mdx

index 6a9d7d56e378a4261a29b36463ecd8613525b9cd..c8cf38344e0e638bce9675fd0f3f8e37f1fac094 100644 (file)
@@ -25,7 +25,7 @@ $accordion-tokens: defaults(
     --accordion-btn-icon-width: 1rem,
     --accordion-btn-icon-transform: rotate(-180deg),
     --accordion-btn-icon-transition: transform .2s ease-in-out,
-    --accordion-active-color: var(--fg),
+    --accordion-active-color: var(--fg-body),
     --accordion-active-bg: var(--bg-2),
   ),
   $accordion-tokens
index 3bd0962005dd2a7fef7030bd662634d994c1ef75..96aa6393105ecb5c0f441ddd35fae599a2dd9a80 100644 (file)
@@ -208,16 +208,16 @@ $utilities: map.merge(
     // scss-docs-start utils-border-color
     "border-color": (
       property: (
-        "--border-color": null,
-        "border-color": var(--border-color)
+        "--bc": null,
+        "border-color": var(--bc)
       ),
       class: border,
       values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-borders, "border")),
     ),
     "border-color-subtle": (
       property: (
-        "--border-color": null,
-        "border-color": var(--border-color)
+        "--bc": null,
+        "border-color": var(--bc)
       ),
       class: border-subtle,
       values: theme-color-refs("border"),
@@ -230,7 +230,7 @@ $utilities: map.merge(
     "border-opacity": (
       class: border,
       property: border-color,
-      values: theme-opacity-values(--border-color)
+      values: theme-opacity-values(--bc)
     ),
     // scss-docs-end utils-border-color
     // Sizing utilities
index 2d78150d7bff7562badd25c0cf2c41c2621b30c8..9076fc3b8b3d17823a47cd0944ec9acefc11cf5c 100644 (file)
@@ -1,6 +1,7 @@
 @use "sass:list";
 @use "sass:map";
 @use "sass:meta";
+@use "sass:string";
 @use "../layout/breakpoints" as bp;
 
 // Utility generator
   }
 }
 
+// Helper: append a custom property name to a list only if not already present.
+@function append-unique-var($list, $name) {
+  @if list.index($list, $name) == null {
+    @return list.append($list, $name);
+  }
+  @return $list;
+}
+
+// Helper: collect custom property names (those starting with `--`) from a
+// utility's `property` value, which may be a map, list, or string.
+@function collect-property-vars($list, $properties) {
+  @if meta.type-of($properties) == "map" {
+    @each $prop, $value in $properties {
+      @if meta.type-of($prop) == "string" and string.slice($prop, 1, 2) == "--" {
+        $list: append-unique-var($list, $prop);
+      }
+    }
+  } @else if meta.type-of($properties) == "list" {
+    @each $prop in $properties {
+      @if meta.type-of($prop) == "string" and string.slice($prop, 1, 2) == "--" {
+        $list: append-unique-var($list, $prop);
+      }
+    }
+  } @else if meta.type-of($properties) == "string" and string.slice($properties, 1, 2) == "--" {
+    $list: append-unique-var($list, $properties);
+  }
+  @return $list;
+}
+
+// Registers every custom property that utilities assign to as non-inheriting
+// via `@property`. Utilities compose values through local custom properties
+// (e.g. `--bc`, `--bg`, `--fg`, `--rounded-size`); because custom properties
+// inherit by default, setting one on an element would otherwise leak into every
+// descendant (e.g. `.border-inverse` tinting nested component borders). Marking
+// them `inherits: false` scopes each utility to the element it's applied to.
+// `syntax: "*"` is used so no `initial-value` is required (values are complex
+// expressions like `light-dark()` / `color-mix()` that can't be registered).
+@mixin generate-utility-at-properties($utilities) {
+  $names: ();
+
+  @each $key, $utility in $utilities {
+    @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false {
+      // Custom properties assigned via the `property` key
+      $names: collect-property-vars($names, map.get($utility, property));
+
+      // Custom properties assigned via the `variables` key (names lack the `--` prefix)
+      @if map.has-key($utility, variables) {
+        $variables: map.get($utility, variables);
+        @if meta.type-of($variables) == "map" {
+          @each $var-key, $var-value in $variables {
+            $names: append-unique-var($names, "--" + $var-key);
+          }
+        } @else if meta.type-of($variables) == "list" {
+          @each $var-name in $variables {
+            $names: append-unique-var($names, "--" + $var-name);
+          }
+        } @else {
+          $names: append-unique-var($names, "--" + $variables);
+        }
+      }
+    }
+  }
+
+  @each $name in $names {
+    @property #{$name} {
+      syntax: "*";
+      inherits: false;
+    }
+  }
+}
+
 // 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.
index 327573a398ab630c4a34483f862c5738915bda10..569ce6a402f68794df04882ef40e695ff05f3ae9 100644 (file)
@@ -2,6 +2,11 @@
 @use "../mixins/utilities" as *;
 @use "../utilities" as *;
 
+// Register utility-composition custom properties as non-inheriting so utilities
+// (e.g. `.border-inverse`) don't leak their composed value into descendants.
+// Emitted outside the cascade layer since `@property` registration is global.
+@include generate-utility-at-properties($utilities);
+
 @layer utilities {
   @include generate-utilities-loop($utilities, $breakpoints);
 }
index 921620966378dc2c16cae46a2f0200f357c5b785..7f5c953f04b0cc27e1f31787ed8533f2d4cd16aa 100644 (file)
@@ -15,19 +15,23 @@ import { getData } from '@libs/data'
 
 ## Colors
 
-Change the `border-color` using utilities built on our theme colors using `.border-{color}` and `.border-subtle-{color}`. All these border color utilities set the `border-color` property to a local CSS variable, `--bs-border-color`, which has the value of the theme color. The root color tokens color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
+Change the `border-color` using utilities built on our theme colors using `.border-{color}` and `.border-subtle-{color}`. All these border color utilities set the `border-color` property to a local CSS variable, `--bs-bc`, which has the value of the theme color. The root color tokens color values also use `light-dark()` to ensure sufficient contrast in both light and dark color modes.
 
-For example, `.border-primary` sets the `--bs-border-color` variable to `var(--bs-primary-border)`:
+For example, `.border-primary` sets the `--bs-bc` variable to `var(--bs-primary-border)`:
 
 ```css
 .border-primary {
-  --bs-border-color: var(--bs-primary-border);
-  border-color: var(--bs-border-color);
+  --bs-bc: var(--bs-primary-border);
+  border-color: var(--bs-bc);
 }
 ```
 
 This approach allows us to also easily support translucency with our `.border-{opacity}` utilities as we can use `color-mix()` with the CSS variable to generate the appropriate color. See the [opacity section](#opacity) for more details.
 
+<Callout>
+The `--bs-bc` variable is registered with `@property` as non-inheriting (`inherits: false`), so border color utilities are scoped to the element they’re applied to and never cascade into nested elements or components. This is why `--bs-bc` is deliberately separate from the inheriting `--bs-border-color` design token that components resolve their default borders from.
+</Callout>
+
 <Example class="d-flex flex-column gap-2 bd-example-border-color-utils" code={[
   ...getData('theme-colors').map((themeColor) => `<div class="border border-5 border-${themeColor.name}">${themeColor.name}</div>
 <div class="border border-5 border-subtle-${themeColor.name}">${themeColor.name} subtle</div>`),
@@ -55,9 +59,20 @@ Or modify the default `border-color` of a component:
     Changing border color and width
   </div>`} />
 
+## Nested elements
+
+Because border color utilities set the non-inheriting `--bs-bc` variable, parent border colors don’t affect the borders of nested elements. Below, the parent uses `.border-danger` while the nested `.border` keeps the default border color.
+
+<Example code={`<div class="p-3 border border-danger rounded-5">
+    Parent with .border-danger
+    <div class="border p-3 mt-3">
+      Nested .border keeps the default color
+    </div>
+  </div>`} />
+
 ## Opacity
 
-Change the opacity of a border color by using any of the `.border-<percentage>` utilities which use `color-mix()` to mix the border color with `transparent` thanks to the CSS variable approach mentioned above.
+Change the opacity of a border color by using any of the `.border-<percentage>` utilities which use `color-mix()` to mix the border color with `transparent` thanks to the CSS variable approach mentioned above. Because `--bs-bc` is scoped to the element, pair these opacity utilities with a `.border-{color}` utility on the same element—they no longer inherit a border color from an ancestor.
 
 <Example code={`<div class="border border-5 border-primary p-2 mb-2">Default border</div>
 <div class="border border-5 border-primary border-90 p-2 mb-2">90% opacity</div>
index 60f261337a69d864a4cfc1c7e05fa4397f4b5e33..12a35b540ff5901703cf617652e231c7d7bb6735 100644 (file)
@@ -40,6 +40,8 @@ Or remove borders:
 
 ## Width
 
+Border width utilities are available in increments of 1px from 0 to 5px.
+
 <Example class="bd-example-border-utils" code={`<span class="border border-1"></span>
 <span class="border border-2"></span>
 <span class="border border-3"></span>
index 2b8e86808fe94b7ae1914ba4ae059c1a871808f8..540fb8a49429e21d4be0c23183c58f362d2b08b0 100644 (file)
@@ -65,6 +65,10 @@ Divide utilities apply `border-inline-start` or `border-block-start` to every di
 }
 ```
 
+<Callout>
+Divider color comes from the inheriting `--bs-border-color` design token, so set it on the parent to recolor dividers. Note this differs from the `.border-{color}` utilities, which are element-scoped (they set the non-inheriting `--bs-bc` variable) and therefore won’t recolor a parent’s dividers.
+</Callout>
+
 ## CSS
 
 ### Sass utilities API