]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Update utility API for children selectors (#42224)
authorMark Otto <markd.otto@gmail.com>
Fri, 3 Apr 2026 22:20:49 +0000 (15:20 -0700)
committerGitHub <noreply@github.com>
Fri, 3 Apr 2026 22:20:49 +0000 (15:20 -0700)
* Update utility API for children selectors

- Add new support for custom children selectors
- Add divide and space utilities

* Update bundlewatch

* metadata

.bundlewatch.config.json
dist/css/bootstrap-utilities.metadata.json
scss/_utilities.scss
scss/mixins/_utilities.scss
site/data/sidebar.yml
site/src/components/UtilityReferenceTable.astro
site/src/content/docs/migration.mdx
site/src/content/docs/utilities/api.mdx
site/src/content/docs/utilities/divide.mdx [new file with mode: 0644]
site/src/content/docs/utilities/space.mdx [new file with mode: 0644]

index 394f454500a03c31ed2f791b09c57939f1708867..7d1e7bd3f1905c95ce3dd090ec3252ed40f42a3f 100644 (file)
     },
     {
       "path": "./dist/css/bootstrap-utilities.css",
-      "maxSize": "16.75 kB"
+      "maxSize": "17.75 kB"
     },
     {
       "path": "./dist/css/bootstrap-utilities.min.css",
-      "maxSize": "16.0 kB"
+      "maxSize": "16.75 kB"
     },
     {
       "path": "./dist/css/bootstrap.css",
-      "maxSize": "46.75 kB"
+      "maxSize": "47.75 kB"
     },
     {
       "path": "./dist/css/bootstrap.min.css",
-      "maxSize": "44.0 kB"
+      "maxSize": "44.75 kB"
     },
     {
       "path": "./dist/js/bootstrap.bundle.js",
index 03dab1a58077191d7e1916ede655cee930360772..c44996cc4fdaaf44d930c1c4a21a885f1687c94c 100644 (file)
         "column-gap-9"
       ]
     },
+    "space-x": {
+      "class": "space-x",
+      "property": "margin-inline-end",
+      "classes": [
+        "space-x-0",
+        "space-x-1",
+        "space-x-2",
+        "space-x-3",
+        "space-x-4",
+        "space-x-5",
+        "space-x-6",
+        "space-x-7",
+        "space-x-8",
+        "space-x-9"
+      ]
+    },
+    "space-y": {
+      "class": "space-y",
+      "property": "margin-block-end",
+      "classes": [
+        "space-y-0",
+        "space-y-1",
+        "space-y-2",
+        "space-y-3",
+        "space-y-4",
+        "space-y-5",
+        "space-y-6",
+        "space-y-7",
+        "space-y-8",
+        "space-y-9"
+      ]
+    },
+    "divide-x": {
+      "class": "divide-x",
+      "property": "border-inline-start",
+      "classes": [
+        "divide-x",
+        "divide-x-0"
+      ]
+    },
+    "divide-y": {
+      "class": "divide-y",
+      "property": "border-block-start",
+      "classes": [
+        "divide-y",
+        "divide-y-0"
+      ]
+    },
     "font-family": {
       "class": "font",
       "property": "font-family",
index fae29c335554a0f6c3518e87d91d51c5511067d3..533ad3a43039ecf37b26bcc4eb9a99dd5b6c58be 100644 (file)
@@ -582,6 +582,44 @@ $utilities: map.merge(
       values: $spacers
     ),
     // scss-docs-end utils-spacing
+    // scss-docs-start utils-space
+    "space-x": (
+      responsive: true,
+      property: margin-inline-end,
+      class: space-x,
+      child-selector: "> :not(:last-child)",
+      values: $spacers
+    ),
+    "space-y": (
+      responsive: true,
+      property: margin-block-end,
+      class: space-y,
+      child-selector: "> :not(:last-child)",
+      values: $spacers
+    ),
+    // scss-docs-end utils-space
+    // scss-docs-start utils-divide
+    "divide-x": (
+      responsive: true,
+      property: border-inline-start,
+      class: divide-x,
+      child-selector: "> :not(:first-child)",
+      values: (
+        null: var(--border-width) var(--border-style) var(--border-color),
+        0: 0,
+      )
+    ),
+    "divide-y": (
+      responsive: true,
+      property: border-block-start,
+      class: divide-y,
+      child-selector: "> :not(:first-child)",
+      values: (
+        null: var(--border-width) var(--border-style) var(--border-color),
+        0: 0,
+      )
+    ),
+    // scss-docs-end utils-divide
     // Text
     // scss-docs-start utils-font-family
     "font-family": (
index 1377d24e5f52d8877ee7f953647c749e2cebc67c..7b523ff3c817bc12c808c22cf3d4131fcf85f64b 100644 (file)
@@ -10,6 +10,7 @@
 //   - class: .class
 //   - attr-starts: [class^="class"]
 //   - attr-includes: [class*="class"]
+// - Utilities can target children via `child-selector`, wrapped in :where() for zero specificity
 // - Utilities can generate regular CSS properties and CSS custom properties
 // - Utilities can be responsive or not
 // - Utilities can have state variants (e.g., hover, focus, active)
@@ -90,7 +91,7 @@
   }
 
   // Warn on unknown keys (likely typos)
-  $valid-keys: property, values, class, selector, responsive, print, important, state, variables;
+  $valid-keys: property, values, class, selector, responsive, print, important, state, variables, child-selector;
   @each $key in map.keys($utility) {
     @if not list.index($valid-keys, $key) {
       @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}";
     // @debug $properties;
     // @debug $values;
 
-    #{$selector} {
+    // Apply child-selector wrapping if present (wraps in :where() for zero specificity)
+    $child-sel: null;
+    @if map.has-key($utility, child-selector) {
+      $child-sel: map.get($utility, child-selector);
+    }
+
+    $final-selector: $selector;
+    @if $child-sel {
+      $final-selector: ":where(#{$selector} #{$child-sel})";
+    }
+
+    #{$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) {
     // Generate state variants
     @if $state != () {
       @each $state-variant in $state {
-        #{$selector}-#{$state-variant}:#{$state-variant} {
+        $state-selector: "#{$selector}-#{$state-variant}:#{$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);
index 44d76293215aa0720f5e537b935e00d5f9d6f724..fbf96b804aa663a255c379824ffb90903b1441ee 100644 (file)
       pages:
         - title: Margin
         - title: Padding
+        - title: Space
     - group: Type
       pages:
         - title: Font family
         - title: Border
         - title: Border color
         - title: Border radius
+        - title: Divide
     - group: Interactions
       pages:
         - title: Pointer events
index 14ab1717b914e47124601c4709d27bb6ed630222..1b5669f4ae2c4e74b34946a2f6bd07662f4a9450 100644 (file)
@@ -67,9 +67,12 @@ function parseCompiledCSS(classNames: string[]): Record<string, string[]> {
     const classStyles: Record<string, string[]> = {};
 
     classNames.forEach(className => {
-      // Match ONLY single class selectors: .classname { declarations }
+      // Match class selectors, including :where() wrapped child-selector utilities
       const escapedClass = className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-      const selectorRegex = new RegExp(`(?:^|\\n)\\s*\\.${escapedClass}\\s*\\{([^}]+)\\}`, 'gm');
+      const selectorRegex = new RegExp(
+        `(?:^|\\n)\\s*(?::where\\()?\\s*\\.${escapedClass}(?:\\s[^{]*)?\\)?\\s*\\{([^}]+)\\}`,
+        'gm'
+      );
 
       let match;
       let foundDeclarations: string[] = [];
index a2f957b87e717880d09f0c16f1074a70f6040d68..686777232fe27913a5692f2e515ddf8ddf9789a7 100644 (file)
@@ -201,6 +201,9 @@ Bootstrap 6 is a major release with many breaking changes to modernize our codeb
   - Added `auto`, `min-content`, `max-content`, and `fit-content` to `width` and `height` utilities.
 - **Flex & Grid utilities:**
   - Added `.place-items` and `.justify-items` utilities.
+- **Utilities API:** Added new `child-selector` option that enables parent-to-child selector patterns wrapped in `:where()` for zero specificity. Any CSS child/descendant selector can be used.
+- **Space utilities:** Added `.space-x-*` and `.space-y-*` utilities for margin-based spacing between direct children of an element. Responsive variants included.
+- **Divide utilities:** Added `.divide-x` and `.divide-y` utilities for adding borders between direct children of an element, using existing border CSS variables. Responsive variants included.
 
 ### Docs
 
index fc2bcfd4ea2a611e74dd23b5cd4fd31ae679804f..0dd08f5cdb25fa846ea717f70ae7dbb44f3f5fa0 100644 (file)
@@ -15,6 +15,7 @@ The `$utilities` map contains all our utilities and is later merged with your cu
 | [`property`](#property) | **Required** | – | Name of the property, this can be a string or an array of strings (e.g., horizontal paddings or margins). |
 | [`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. |
 | [`class`](#class) | Optional | null | Name of the generated class. If not provided and `property` is an array of strings, `class` will default to the first element of the `property` array. If not provided and `property` is a string, the `values` keys are used for the `class` names. |
 | [`css-var`](#css-variable-utilities) | Optional | `false` | Boolean to generate CSS variables instead of CSS rules. |
 | [`css-variable-name`](#css-variable-utilities) | Optional | null | Custom un-prefixed name for the CSS variable inside the ruleset. |
@@ -155,6 +156,79 @@ Which outputs the following:
 .ratio-21x9 { --bs-ratio: 21 / 9; }
 ```
 
+### `child-selector`
+
+Use the `child-selector` option to apply a CSS property to children of the element with the utility class rather than the element itself. The generated selector is wrapped in `:where()` for zero specificity, making it easy to override. This powers our built-in [`space-x/y`]([[docsref:/utilities/space]]) and [`divide-x/y`]([[docsref:/utilities/divide]]) utilities.
+
+For example, our `space-x` utility uses `child-selector` to add horizontal spacing between direct children:
+
+```scss
+$utilities: (
+  "space-x": (
+    property: margin-inline-end,
+    class: space-x,
+    child-selector: "> :not(:last-child)",
+    values: (
+      1: .25rem,
+      2: .5rem,
+      3: 1rem,
+    )
+  )
+);
+```
+
+Output:
+
+```css
+:where(.space-x-1 > :not(:last-child)) { margin-inline-end: .25rem; }
+:where(.space-x-2 > :not(:last-child)) { margin-inline-end: .5rem; }
+:where(.space-x-3 > :not(:last-child)) { margin-inline-end: 1rem; }
+```
+
+The `child-selector` value can be any valid CSS selector. For example, you could create a striped row utility using `:nth-child()`:
+
+```scss
+$utilities: (
+  "striped-bg": (
+    property: background-color,
+    class: striped,
+    child-selector: "> :nth-child(odd)",
+    values: (
+      null: var(--bs-tertiary-bg),
+    )
+  )
+);
+```
+
+Output:
+
+```css
+:where(.striped > :nth-child(odd)) { background-color: var(--bs-tertiary-bg); }
+```
+
+Or target all direct children with `> *`:
+
+```scss
+$utilities: (
+  "child-rounded": (
+    property: border-radius,
+    class: child-rounded,
+    child-selector: "> *",
+    values: (
+      null: var(--bs-border-radius),
+      0: 0,
+    )
+  )
+);
+```
+
+Output:
+
+```css
+:where(.child-rounded > *) { border-radius: var(--bs-border-radius); }
+:where(.child-rounded-0 > *) { border-radius: 0; }
+```
+
 ### `class`
 
 Use the `class` option to change the class prefix used in the compiled CSS. For example, to change from `.opacity-*` to `.o-*`:
diff --git a/site/src/content/docs/utilities/divide.mdx b/site/src/content/docs/utilities/divide.mdx
new file mode 100644 (file)
index 0000000..2b8e868
--- /dev/null
@@ -0,0 +1,97 @@
+---
+title: Divide
+description: Divide content with borders between direct children of an element. Supports horizontal and vertical dividers with responsive variants.
+toc: true
+mdn: https://developer.mozilla.org/en-US/docs/Web/CSS/border
+utility:
+  - divide-x
+  - divide-y
+---
+
+import { getData } from '@libs/data'
+
+## Example
+
+Rather than adding border classes to individual children, use `.divide-x` or `.divide-y` on the parent.
+
+<Example class="d-flex align-items-center justify-content-center text-center" code={`
+  <div class="d-flex divide-x">
+    <div class="px-3 py-2">Item 1</div>
+    <div class="px-3 py-2">Item 2</div>
+    <div class="px-3 py-2">Item 3</div>
+  </div>`} />
+
+The selectors generated for divider utilities are wrapped in `:where()` for zero specificity, making them easy to override with additional utilities or custom CSS.
+
+## Horizontal
+
+Use `.divide-x` to add vertical border lines between horizontally arranged children:
+
+<Example class="d-flex align-items-center justify-content-center text-center" code={`
+  <div class="d-flex divide-x rounded-3 border">
+    <div class="px-4 py-3">One</div>
+    <div class="px-4 py-3">Two</div>
+    <div class="px-4 py-3">Three</div>
+  </div>`} />
+
+## Vertical
+
+Use `.divide-y` to add horizontal border lines between vertically stacked children:
+
+<Example class="d-flex align-items-center justify-content-center text-center" code={`
+  <div class="divide-y rounded-3 border d-inline-block">
+    <div class="px-4 py-3">One</div>
+    <div class="px-4 py-3">Two</div>
+    <div class="px-4 py-3">Three</div>
+  </div>`} />
+
+### Removing dividers
+
+Use `.divide-x-0` or `.divide-y-0` to remove dividers, which is particularly useful at specific breakpoints:
+
+```html
+<div class="divide-y divide-y-md-0 divide-x-md">
+  ...
+</div>
+```
+
+### How it works
+
+Divide utilities apply `border-inline-start` or `border-block-start` to every direct child except the first via the selector `:where(.divide-y > :not(:first-child))`. The `:where()` wrapper keeps specificity at zero. Border styling is controlled by the existing `--border-width`, `--border-style`, and `--border-color` CSS variables.
+
+```css
+:where(.divide-y > :not(:first-child)) {
+  border-block-start: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color);
+}
+```
+
+## CSS
+
+### Sass utilities API
+
+Divide 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
+// scss-docs-start utils-divide
+"divide-x": (
+  responsive: true,
+  property: border-inline-start,
+  class: divide-x,
+  child-selector: "> :not(:first-child)",
+  values: (
+    null: var(--border-width) var(--border-style) var(--border-color),
+    0: 0,
+  )
+),
+"divide-y": (
+  responsive: true,
+  property: border-block-start,
+  class: divide-y,
+  child-selector: "> :not(:first-child)",
+  values: (
+    null: var(--border-width) var(--border-style) var(--border-color),
+    0: 0,
+  )
+),
+// scss-docs-end utils-divide
+```
diff --git a/site/src/content/docs/utilities/space.mdx b/site/src/content/docs/utilities/space.mdx
new file mode 100644 (file)
index 0000000..9ad3c66
--- /dev/null
@@ -0,0 +1,119 @@
+---
+title: Space
+description: Control the margin between direct children of an element in non-flex and non-grid layouts.
+toc: true
+mdn: https://developer.mozilla.org/en-US/docs/Web/CSS/margin
+utility:
+  - space-x
+  - space-y
+---
+
+import { getData } from '@libs/data'
+
+## Example
+
+Unlike [gap utilities]([[docsref:/utilities/gap]]) which require flexbox or grid, space utilities work on any parent element by applying margins to children.
+
+<Example class="d-flex align-items-center justify-content-center gap-4 text-center" code={`
+  <div class="d-flex space-x-3 p-3 bd-pattern-diagonal rounded-3">
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 1</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 2</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 3</div>
+  </div>`} />
+
+The selectors generated for space utilities are wrapped in `:where()` for zero specificity, making them easy to override with additional utilities or custom CSS.
+
+## Notation
+
+Space utilities that apply to all breakpoints, from `xs` to `2xl`, have no breakpoint abbreviation in them. This is because those classes are applied from `min-width: 0` and up, and thus are not bound by a media query. The remaining breakpoints, however, do include a breakpoint abbreviation.
+
+The classes are named using the format `{property}-{size}` for `xs` and `{property}-{breakpoint}-{size}` for `sm`, `md`, `lg`, `xl`, and `2xl`.
+
+Where *property* is one of:
+
+- `space-x` - for classes that set horizontal spacing via `margin-inline-end`
+- `space-y` - for classes that set vertical spacing via `margin-block-end`
+
+Where *size* is one of:
+
+- `0` - for classes that eliminate the spacing by setting it to `0`
+- `1` - (by default) for classes that set the spacing to `$spacer * .25`
+- `2` - (by default) for classes that set the spacing to `$spacer * .5`
+- `3` - (by default) for classes that set the spacing to `$spacer`
+- `4` - (by default) for classes that set the spacing to `$spacer * 1.5`
+- `5` - (by default) for classes that set the spacing to `$spacer * 3`
+
+(You can add more sizes by adding entries to the `$spacers` Sass map variable.)
+
+## Horizontal
+
+Use `space-x-*` utilities to control the horizontal space between children:
+
+<Example showMarkup={false} class="d-flex align-items-center justify-content-center text-center" code={`
+  <div class="d-flex space-x-3 bd-pattern-diagonal rounded-3">
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 1</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 2</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 3</div>
+  </div>`} />
+
+```html
+<div class="d-flex space-x-3">
+  <div>Item 1</div>
+  <div>Item 2</div>
+  <div>Item 3</div>
+</div>
+```
+
+## Vertical
+
+Use `space-y-*` utilities to control the vertical space between children:
+
+<Example showMarkup={false} class="d-flex align-items-center justify-content-center text-center" code={`
+  <div class="space-y-3 bd-pattern-diagonal rounded-3 d-inline-block p-3">
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 1</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 2</div>
+    <div class="p-3 bg-subtle-accent fg-accent rounded-3">Item 3</div>
+  </div>`} />
+
+```html
+<div class="space-y-3">
+  <div>Item 1</div>
+  <div>Item 2</div>
+  <div>Item 3</div>
+</div>
+```
+
+## How it works
+
+Space utilities apply `margin-inline-end` or `margin-block-end` to every direct child except the last one via the selector `:where(.space-x-3 > :not(:last-child))`. The `:where()` wrapper keeps specificity at zero, so you can easily override individual children with standard margin utilities.
+
+```css
+:where(.space-x-3 > :not(:last-child)) {
+  margin-inline-end: 1rem;
+}
+```
+
+## CSS
+
+### Sass utilities API
+
+Space 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
+// scss-docs-start utils-space
+"space-x": (
+  responsive: true,
+  property: margin-inline-end,
+  class: space-x,
+  child-selector: "> :not(:last-child)",
+  values: $spacers
+),
+"space-y": (
+  responsive: true,
+  property: margin-block-end,
+  class: space-y,
+  child-selector: "> :not(:last-child)",
+  values: $spacers
+),
+// scss-docs-end utils-space
+```