diff --git a/scss/_utilities.scss b/scss/_utilities.scss index 39211802d254..5c211efaadee 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -579,6 +579,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 ab82b2e37f71..58b64673a1b8 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}"; @@ -206,7 +207,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) { @@ -229,7 +241,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 55a22c3596c2..5b2308aac74a 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -180,6 +180,7 @@ pages: - title: Margin - title: Padding + - title: Space - group: Type pages: - title: Font family @@ -199,6 +200,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 14ab1717b914..1b5669f4ae2c 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 0d70f4d5a633..d5af3f74b68f 100644 --- a/site/src/content/docs/migration.mdx +++ b/site/src/content/docs/migration.mdx @@ -150,6 +150,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 677080b11a85..4e5e3c4cbe15 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. | @@ -156,6 +157,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 000000000000..2b8e86808fe9 --- /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 000000000000..9ad3c66a6e2a --- /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 +```