Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions scss/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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": (
Expand Down
23 changes: 20 additions & 3 deletions scss/mixins/_utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}";
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions site/data/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
pages:
- title: Margin
- title: Padding
- title: Space
- group: Type
pages:
- title: Font family
Expand All @@ -199,6 +200,7 @@
- title: Border
- title: Border color
- title: Border radius
- title: Divide
- group: Interactions
pages:
- title: Pointer events
Expand Down
7 changes: 5 additions & 2 deletions site/src/components/UtilityReferenceTable.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
3 changes: 3 additions & 0 deletions site/src/content/docs/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
74 changes: 74 additions & 0 deletions site/src/content/docs/utilities/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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-*`:
Expand Down
97 changes: 97 additions & 0 deletions site/src/content/docs/utilities/divide.mdx
Original file line number Diff line number Diff line change
@@ -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
```
Loading
Loading