From: Mark Otto Date: Fri, 3 Apr 2026 22:20:49 +0000 (-0700) Subject: Update utility API for children selectors (#42224) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1d076309193022a11f9a17ed305830d047285b3f;p=thirdparty%2Fbootstrap.git Update utility API for children selectors (#42224) * Update utility API for children selectors - Add new support for custom children selectors - Add divide and space utilities * Update bundlewatch * metadata --- diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 394f454500..7d1e7bd3f1 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -18,19 +18,19 @@ }, { "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", diff --git a/dist/css/bootstrap-utilities.metadata.json b/dist/css/bootstrap-utilities.metadata.json index 03dab1a580..c44996cc4f 100644 --- a/dist/css/bootstrap-utilities.metadata.json +++ b/dist/css/bootstrap-utilities.metadata.json @@ -850,6 +850,54 @@ "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", diff --git a/scss/_utilities.scss b/scss/_utilities.scss index fae29c3355..533ad3a430 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -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": ( diff --git a/scss/mixins/_utilities.scss b/scss/mixins/_utilities.scss index 1377d24e5f..7b523ff3c8 100644 --- a/scss/mixins/_utilities.scss +++ b/scss/mixins/_utilities.scss @@ -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}"; @@ -194,7 +195,18 @@ // @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) { @@ -217,7 +229,12 @@ // 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); diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index 44d7629321..fbf96b804a 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -191,6 +191,7 @@ pages: - title: Margin - title: Padding + - title: Space - group: Type pages: - title: Font family @@ -210,6 +211,7 @@ - title: Border - title: Border color - title: Border radius + - title: Divide - group: Interactions pages: - title: Pointer events diff --git a/site/src/components/UtilityReferenceTable.astro b/site/src/components/UtilityReferenceTable.astro index 14ab1717b9..1b5669f4ae 100644 --- a/site/src/components/UtilityReferenceTable.astro +++ b/site/src/components/UtilityReferenceTable.astro @@ -67,9 +67,12 @@ function parseCompiledCSS(classNames: string[]): Record { const classStyles: Record = {}; 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[] = []; diff --git a/site/src/content/docs/migration.mdx b/site/src/content/docs/migration.mdx index a2f957b87e..686777232f 100644 --- a/site/src/content/docs/migration.mdx +++ b/site/src/content/docs/migration.mdx @@ -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 diff --git a/site/src/content/docs/utilities/api.mdx b/site/src/content/docs/utilities/api.mdx index fc2bcfd4ea..0dd08f5cdb 100644 --- a/site/src/content/docs/utilities/api.mdx +++ b/site/src/content/docs/utilities/api.mdx @@ -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 index 0000000000..2b8e86808f --- /dev/null +++ b/site/src/content/docs/utilities/divide.mdx @@ -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. + + +
Item 1
+
Item 2
+
Item 3
+ `} /> + +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: + + +
One
+
Two
+
Three
+ `} /> + +## Vertical + +Use `.divide-y` to add horizontal border lines between vertically stacked children: + + +
One
+
Two
+
Three
+ `} /> + +### Removing dividers + +Use `.divide-x-0` or `.divide-y-0` to remove dividers, which is particularly useful at specific breakpoints: + +```html +
+ ... +
+``` + +### 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 index 0000000000..9ad3c66a6e --- /dev/null +++ b/site/src/content/docs/utilities/space.mdx @@ -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. + + +
Item 1
+
Item 2
+
Item 3
+ `} /> + +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: + + +
Item 1
+
Item 2
+
Item 3
+ `} /> + +```html +
+
Item 1
+
Item 2
+
Item 3
+
+``` + +## Vertical + +Use `space-y-*` utilities to control the vertical space between children: + + +
Item 1
+
Item 2
+
Item 3
+ `} /> + +```html +
+
Item 1
+
Item 2
+
Item 3
+
+``` + +## 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 +```