},
{
"path": "./dist/css/bootstrap-utilities.css",
- "maxSize": "17.75 kB"
+ "maxSize": "18.0 kB"
},
{
"path": "./dist/css/bootstrap-utilities.min.css",
},
{
"path": "./dist/css/bootstrap.css",
- "maxSize": "48.25 kB"
+ "maxSize": "48.5 kB"
},
{
"path": "./dist/css/bootstrap.min.css",
- "maxSize": "45.0 kB"
+ "maxSize": "45.25 kB"
},
{
"path": "./dist/js/bootstrap.bundle.js",
values: $position-values
),
"start": (
- property: left,
+ property: inset-inline-start,
class: start,
values: $position-values
),
"end": (
- property: right,
+ property: inset-inline-end,
class: end,
values: $position-values
),
)
),
"border-y": (
+ class: border-y,
property: border-block,
values: (
null: var(--border-width) var(--border-style) var(--border-color),
)
),
"border-x": (
+ class: border-x,
property: border-inline,
values: (
null: var(--border-width) var(--border-style) var(--border-color),
responsive: true,
property: justify-self,
values: (
- start: flex-start,
- end: flex-end,
+ start: start,
+ end: end,
center: center,
)
),
values: lowercase uppercase capitalize
),
// scss-docs-end utils-text-transform
- // "white-space": (
- // property: white-space,
- // class: text,
- // values: (
- // wrap: normal,
- // nowrap: nowrap,
- // )
- // ),
+ // scss-docs-start utils-text-wrap
"text-wrap": (
- property: white-space,
+ property: text-wrap,
class: text,
values: wrap nowrap balance pretty,
),
+ // scss-docs-end utils-text-wrap
// scss-docs-start utils-text-break
"word-wrap": (
property: word-wrap word-break,
// scss-docs-end utils-text-break
// scss-docs-end utils-text
// scss-docs-start utils-color
- // "color-attr": (
- // selector: "attr-includes",
- // class: "fg-",
- // property: color,
- // values: var(--fg),
- // ),
"fg": (
property: (
"--fg": null,
// scss-docs-end utils-interaction
// scss-docs-start utils-border-radius
"border-radius": (
- property: border-radius,
+ property: (
+ "--rounded-size": null,
+ "border-radius": var(--rounded-size)
+ ),
class: rounded,
values: map.merge(
$radii,
)
)
),
+ "rounded-size": (
+ property: --rounded-size,
+ class: rounded-size,
+ values: map.merge(
+ $radii,
+ (
+ null: map.get($radii, 5),
+ circle: 50%,
+ pill: var(--radius-pill)
+ )
+ )
+ ),
"rounded-top": (
property: border-start-start-radius border-start-end-radius,
class: rounded-top,
values: map.merge(
$radii,
(
- null: map.get($radii, 5),
+ null: var(--rounded-size, var(--radius-5)),
circle: 50%,
pill: var(--radius-pill)
)
)
),
"rounded-end": (
- property: border-end-end-radius border-end-start-radius,
+ property: border-start-end-radius border-end-end-radius,
class: rounded-end,
values: map.merge(
$radii,
(
- null: map.get($radii, 5),
+ null: var(--rounded-size, var(--radius-5)),
circle: 50%,
pill: var(--radius-pill)
)
)
),
"rounded-bottom": (
- property: border-end-end-radius border-end-start-radius,
+ property: border-end-start-radius border-end-end-radius,
class: rounded-bottom,
values: map.merge(
$radii,
(
- null: map.get($radii, 5),
+ null: var(--rounded-size, var(--radius-5)),
circle: 50%,
pill: var(--radius-pill)
)
)
),
"rounded-start": (
- property: border-start-start-radius border-start-end-radius,
+ property: border-start-start-radius border-end-start-radius,
class: rounded-start,
values: map.merge(
$radii,
(
- null: map.get($radii, 5),
+ null: var(--rounded-size, var(--radius-5)),
circle: 50%,
pill: var(--radius-pill)
)
@use "sass:list";
@use "sass:map";
@use "sass:meta";
-
-// stylelint-disable scss/dollar-variable-pattern
+@use "../layout/breakpoints" as bp;
// Utility generator
// text-decoration-color: ...;
// }
+// Helper mixin to emit CSS custom properties from a utility's `variables` key.
+// When variables is a map, the provided static values are used on each class.
+// When variables is a list or single identifier, each variable receives the current utility value.
+@mixin generate-variables($utility, $value) {
+ @if map.has-key($utility, variables) {
+ $variables: map.get($utility, variables);
+ @if meta.type-of($variables) == "map" {
+ @each $var-key, $var-value in $variables {
+ --#{$var-key}: #{$var-value};
+ }
+ } @else {
+ // Treat as a list (or single identifier) — each variable gets the utility value
+ @each $var-name in $variables {
+ --#{$var-name}: #{$value};
+ }
+ }
+ }
+}
+
// Helper mixin to generate CSS properties for both legacy and property map approaches
-@mixin generate-properties($utility, $propertyMap, $properties, $value) {
- @if $propertyMap != null {
- // New Property-Value Mapping approach
- @each $property, $defaultValue in $propertyMap {
- // If value is a map, check if it has a key for this property
- // Otherwise, use defaultValue (or $value if defaultValue is null)
- $actualValue: $defaultValue;
+@mixin generate-properties($utility, $property-map, $properties, $value) {
+ @if $property-map != null {
+ // Property-Value Mapping approach
+ @each $property, $default-value in $property-map {
+ // If value is a map, check if it has a key for this property.
+ // Otherwise, use default-value (or $value if default-value is null).
+ $actual-value: $default-value;
@if meta.type-of($value) == "map" and map.has-key($value, $property) {
- $actualValue: map.get($value, $property);
- } @else if $defaultValue == null {
- $actualValue: $value;
+ $actual-value: map.get($value, $property);
+ } @else if $default-value == null {
+ $actual-value: $value;
}
@if map.get($utility, important) {
- #{$property}: $actualValue !important; // stylelint-disable-line declaration-no-important
+ #{$property}: $actual-value !important; // stylelint-disable-line declaration-no-important
} @else {
- #{$property}: $actualValue;
+ #{$property}: $actual-value;
}
}
} @else {
}
// Warn on unknown keys (likely typos)
- $valid-keys: property, values, class, selector, responsive, print, important, state, variables, child-selector;
+ $valid-keys: property, values, class, selector, responsive, print, dark, important, state, variables, child-selector, enabled;
@each $key in map.keys($utility) {
@if not list.index($valid-keys, $key) {
@warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}";
}
}
+ // Validate boolean keys
+ @each $bool-key in (responsive, print, dark, important, enabled) {
+ @if map.has-key($utility, $bool-key) {
+ $val: map.get($utility, $bool-key);
+ @if $val != true and $val != false {
+ @error "Utility key `#{$bool-key}` should be a boolean (true or false), got: #{$val}";
+ }
+ }
+ }
+
// Determine if we're generating a class, or an attribute selector
- $selectorType: "class";
+ $selector-type: "class";
@if map.has-key($utility, selector) {
- $selectorType: map.get($utility, selector);
+ $selector-type: map.get($utility, selector);
// Validate selector type
$valid-selectors: "class", "attr-starts", "attr-includes";
- @if not list.index($valid-selectors, $selectorType) {
- @error "Invalid `selector` value `#{$selectorType}`. Must be one of: #{$valid-selectors}";
+ @if not list.index($valid-selectors, $selector-type) {
+ @error "Invalid `selector` value `#{$selector-type}`. Must be one of: #{$valid-selectors}";
}
}
- // Then get the class name to use in a class (e.g., .class) or in a attribute selector (e.g., [class^="class"])
- $selectorClass: map.get($utility, class);
+ // Then get the class name to use in a class (e.g., .class) or in an attribute selector (e.g., [class^="class"])
+ $selector-class: map.get($utility, class);
// Attribute selectors require a `class` key
- @if $selectorType != "class" and not map.has-key($utility, class) {
- @error "Utility with `selector: #{$selectorType}` requires a `class` key.";
+ @if $selector-type != "class" and not map.has-key($utility, class) {
+ @error "Utility with `selector: #{$selector-type}` requires a `class` key.";
}
// Get the list or map of values and ensure it's a map
@each $key, $value in $values {
$properties: map.get($utility, property);
- $propertyMap: null;
- $customClass: "";
+ $property-map: null;
+ $custom-class: "";
- // Check if property is a map (new Property-Value Mapping approach)
+ // Check if property is a map (Property-Value Mapping approach)
@if meta.type-of($properties) == "map" {
- $propertyMap: $properties;
- $customClass: "";
+ $property-map: $properties;
@if map.has-key($utility, class) {
- $customClass: map.get($utility, class);
+ $custom-class: map.get($utility, class);
}
} @else {
- // Legacy approach: Multiple properties are possible, for example with vertical or horizontal margins or paddings
+ // Legacy approach: multiple properties are possible, for example with vertical or horizontal margins or paddings
@if meta.type-of($properties) == "string" {
$properties: list.append((), $properties);
}
// Use custom class if present, otherwise use the first value from the list of properties
@if map.has-key($utility, class) {
- $customClass: map.get($utility, class);
+ $custom-class: map.get($utility, class);
} @else {
- $customClass: list.nth($properties, 1);
+ $custom-class: list.nth($properties, 1);
}
- @if $customClass == null {
- $customClass: "";
+ @if $custom-class == null {
+ $custom-class: "";
}
}
}
// Don't add a dash before value key if value key is null (e.g. with shadow class)
- $customClassModifier: "";
+ $custom-class-modifier: "";
@if $key {
- @if $customClass == "" {
- $customClassModifier: $key;
+ @if $custom-class == "" {
+ $custom-class-modifier: $key;
} @else {
- $customClassModifier: "-" + $key;
+ $custom-class-modifier: "-" + $key;
}
}
// Build the class name fragment (without prefix or dot) for reuse in state variants
- $className: "";
- @if $selectorType == "class" {
- @if $customClass != "" {
- $className: $customClass + $customClassModifier;
- } @else if $selectorClass != null and $selectorClass != "" {
- $className: $selectorClass + $customClassModifier;
+ $class-name: "";
+ @if $selector-type == "class" {
+ @if $custom-class != "" {
+ $class-name: $custom-class + $custom-class-modifier;
+ } @else if $selector-class != null and $selector-class != "" {
+ $class-name: $selector-class + $custom-class-modifier;
} @else {
- $className: $customClassModifier;
+ $class-name: $custom-class-modifier;
}
}
$selector: "";
- @if $selectorType == "class" {
- $selector: ".#{$prefix + $className}";
- } @else if $selectorType == "attr-starts" {
- $selector: "[class^=\"#{$selectorClass}\"]";
- } @else if $selectorType == "attr-includes" {
- $selector: "[class*=\"#{$selectorClass}\"]";
+ @if $selector-type == "class" {
+ $selector: ".#{$prefix + $class-name}";
+ } @else if $selector-type == "attr-starts" {
+ $selector: "[class^=\"#{$selector-class}\"]";
+ } @else if $selector-type == "attr-includes" {
+ $selector: "[class*=\"#{$selector-class}\"]";
}
- // @debug $utility;
- // @debug $selectorType;
- // @debug $selector;
- // @debug $properties;
- // @debug $values;
-
// Apply child-selector wrapping if present (wraps in :where() for zero specificity)
$child-sel: null;
@if map.has-key($utility, child-selector) {
}
#{$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) {
- $variables: map.get($utility, variables);
- @if meta.type-of($variables) == "list" {
- // If variables is a list, each variable gets the utility value
- @each $var-name in $variables {
- --#{$var-name}: #{$value};
- }
- } @else if meta.type-of($variables) == "map" {
- // If variables is a map, use the provided values (for static variables)
- @each $var-key, $var-value in $variables {
- --#{$var-key}: #{$var-value};
- }
- }
- }
- @include generate-properties($utility, $propertyMap, $properties, $value);
+ @include generate-variables($utility, $value);
+ @include generate-properties($utility, $property-map, $properties, $value);
}
// Generate state variants (e.g., hover:link-10 instead of link-10-hover)
@if $state != () {
@each $state-variant in $state {
- $state-selector: ".#{$prefix}#{$state-variant}\\:#{$className}:#{$state-variant}";
+ $state-selector: ".#{$prefix}#{$state-variant}\\:#{$class-name}:#{$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);
- @if meta.type-of($variables) == "list" {
- // If variables is a list, each variable gets the utility value
- @each $var-name in $variables {
- --#{$var-name}: #{$value};
- }
- } @else if meta.type-of($variables) == "map" {
- // If variables is a map, use the provided values (for static variables)
- @each $var-key, $var-value in $variables {
- --#{$var-key}: #{$var-value};
- }
- }
- }
- @include generate-properties($utility, $propertyMap, $properties, $value);
+ @include generate-variables($utility, $value);
+ @include generate-properties($utility, $property-map, $properties, $value);
}
}
}
}
}
+
+// 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.
+@mixin generate-utilities-loop($utilities, $breakpoints) {
+ // Base + responsive (one pass per breakpoint)
+ @each $breakpoint in map.keys($breakpoints) {
+ @include bp.media-breakpoint-up($breakpoint, $breakpoints) {
+ $prefix: bp.breakpoint-prefix($breakpoint, $breakpoints);
+
+ @each $key, $utility in $utilities {
+ @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and (map.get($utility, responsive) or $prefix == "") {
+ @include generate-utility($utility, $prefix);
+ }
+ }
+ }
+ }
+
+ // Print utilities
+ @media print {
+ @each $key, $utility in $utilities {
+ @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, print) == true {
+ @include generate-utility($utility, "print\\:");
+ }
+ }
+ }
+
+ // Dark utilities
+ @media (prefers-color-scheme: dark) {
+ @each $key, $utility in $utilities {
+ @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, dark) == true {
+ @include generate-utility($utility, "dark\\:");
+ }
+ }
+ }
+}
spec_dir: 'scss',
// Make Jasmine look for `.test.scss` files
// spec_files: ['**/*.{test,spec}.scss'],
- spec_files: ['**/_utilities.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'],
+ spec_files: ['**/_utilities.test.scss', '**/utilities/_api.test.scss', '**/modules/_configuration.test.scss', '**/modules/_root-tokens.test.scss', '**/forms/_validation.test.scss'],
// Compile them into JS scripts running `sass-true`
requires: [path.join(__dirname, 'sass-true/register.cjs')],
// Ensure we use `require` so that the require.extensions works
}
}
}
+
+ @include describe("selector") {
+ @include it("generates an attr-starts selector") {
+ @include test-generate-utility(
+ (
+ selector: "attr-starts",
+ class: "ratio-",
+ property: aspect-ratio,
+ values: var(--ratio),
+ )
+ ) {
+ [class^="ratio-"] {
+ aspect-ratio: var(--ratio);
+ }
+ }
+ }
+
+ @include it("generates an attr-includes selector") {
+ @include test-generate-utility(
+ (
+ selector: "attr-includes",
+ class: "ratio-",
+ property: aspect-ratio,
+ values: var(--ratio),
+ )
+ ) {
+ [class*="ratio-"] {
+ aspect-ratio: var(--ratio);
+ }
+ }
+ }
+ }
+
+ @include describe("child-selector") {
+ @include it("wraps the selector in :where() with the child selector appended") {
+ @include test-generate-utility(
+ (
+ property: margin-inline-end,
+ class: space-x,
+ child-selector: "> :not(:last-child)",
+ values: (1: .25rem, 2: .5rem)
+ )
+ ) {
+ :where(.space-x-1 > :not(:last-child)) {
+ margin-inline-end: .25rem;
+ }
+
+ :where(.space-x-2 > :not(:last-child)) {
+ margin-inline-end: .5rem;
+ }
+ }
+ }
+
+ @include it("wraps state variant selectors in :where() as well") {
+ @include test-generate-utility(
+ (
+ property: margin-inline-end,
+ class: space-x,
+ child-selector: "> :not(:last-child)",
+ state: hover,
+ values: (1: .25rem)
+ )
+ ) {
+ :where(.space-x-1 > :not(:last-child)) {
+ margin-inline-end: .25rem;
+ }
+
+ :where(.hover\:space-x-1:hover > :not(:last-child)) {
+ margin-inline-end: .25rem;
+ }
+ }
+ }
+ }
+
+ @include describe("variables") {
+ @include it("emits each list variable with the utility value") {
+ @include test-generate-utility(
+ (
+ property: color,
+ class: link,
+ variables: link-color,
+ values: (10: 10%, 50: 50%)
+ )
+ ) {
+ .link-10 {
+ --link-color: 10%;
+ color: 10%;
+ }
+
+ .link-50 {
+ --link-color: 50%;
+ color: 50%;
+ }
+ }
+ }
+
+ @include it("emits each map variable with its static value") {
+ @include test-generate-utility(
+ (
+ property: text-decoration-color,
+ class: link-underline,
+ variables: ("link-underline-opacity": 1),
+ values: (primary: #06c, danger: #c00)
+ )
+ ) {
+ .link-underline-primary {
+ --link-underline-opacity: 1;
+ text-decoration-color: #06c;
+ }
+
+ .link-underline-danger {
+ --link-underline-opacity: 1;
+ text-decoration-color: #c00;
+ }
+ }
+ }
+
+ @include it("emits variables in state variant blocks too") {
+ @include test-generate-utility(
+ (
+ property: color,
+ class: link,
+ variables: link-color,
+ state: hover,
+ values: (10: 10%)
+ )
+ ) {
+ .link-10 {
+ --link-color: 10%;
+ color: 10%;
+ }
+
+ .hover\:link-10:hover {
+ --link-color: 10%;
+ color: 10%;
+ }
+ }
+ }
+ }
+
+ @include describe("property map") {
+ @include it("uses null default to pass the utility value to a property") {
+ @include test-generate-utility(
+ (
+ property: (
+ "--bg": null,
+ "background-color": var(--bg)
+ ),
+ class: bg,
+ values: (primary: #06c, danger: #c00)
+ )
+ ) {
+ .bg-primary {
+ --bg: #06c;
+ background-color: var(--bg);
+ }
+
+ .bg-danger {
+ --bg: #c00;
+ background-color: var(--bg);
+ }
+ }
+ }
+
+ @include it("uses a per-value map to override individual properties") {
+ @include test-generate-utility(
+ (
+ property: (
+ "font-size": 1rem,
+ "line-height": 1.5
+ ),
+ class: text,
+ values: (
+ "sm": ("font-size": .875rem, "line-height": 1.25),
+ "lg": ("font-size": 1.25rem, "line-height": 1.75),
+ )
+ )
+ ) {
+ .text-sm {
+ font-size: .875rem;
+ line-height: 1.25;
+ }
+
+ .text-lg {
+ font-size: 1.25rem;
+ line-height: 1.75;
+ }
+ }
+ }
+
+ @include it("applies !important to all mapped properties") {
+ @include test-generate-utility(
+ (
+ property: (
+ "--bg": null,
+ "background-color": var(--bg)
+ ),
+ class: bg,
+ important: true,
+ values: (primary: #06c)
+ )
+ ) {
+ .bg-primary {
+ --bg: #06c !important;
+ background-color: var(--bg) !important;
+ }
+ }
+ }
+ }
}
// check-unused-imports-disable — test infrastructure imports.
-@use "../../functions";
-@use "../../config";
-@use "../../maps";
-@use "../../mixins";
-
-$utilities: ();
-
-@include describe("utilities/api") {
- @include it("generates utilities for each breakpoints") {
- $utilities: (
- margin: (
- property: margin,
- important: true,
- values: auto
- ),
- padding: (
- property: padding,
- responsive: true,
- important: true,
- values: 1rem
- ),
- font-size: (
- property: font-size,
- values: (large: 1.25rem),
- important: true,
- print: true
- )
- ) !global;
-
- $breakpoints: (
- xs: 0,
- sm: 333px,
- md: 666px
- ) !global;
-
- @include assert() {
- @include output() {
- @use "../../utilities/api";
- }
+@use "sass:map";
+@use "../../config" as *;
+@use "../../functions" as *;
+@use "../../mixins/utilities" as *;
+
+// Integration-style tests for the utilities API loop.
+// Each test calls generate-utilities-loop — the same mixin that _api.scss calls — so
+// a logic change in the real loop is immediately caught here without any mirrored conditions.
+//
+// A minimal two-breakpoint map is used to keep output predictable and tests fast.
+$test-breakpoints: (
+ xs: 0,
+ sm: 576px,
+);
+
+@include describe("utilities/api loop conditions") {
+ @include describe("enabled") {
+ @include it("skips utilities with enabled: false") {
+ $test-utilities: (
+ opacity: (
+ property: opacity,
+ values: (50: .5)
+ ),
+ float: (
+ property: float,
+ enabled: false,
+ values: (start: inline-start, end: inline-end)
+ )
+ );
- @include expect() {
- // margin is not set to responsive
- .margin-auto {
- margin: auto !important;
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
}
- // padding is, though
- .padding-1rem {
- padding: 1rem !important;
+ @include expect() {
+ .opacity-50 {
+ opacity: .5;
+ }
}
+ }
+ }
- .font-size-large {
- font-size: 1.25rem !important;
+ @include it("outputs utilities without an explicit enabled key") {
+ $test-utilities: (
+ opacity: (
+ property: opacity,
+ values: (25: .25)
+ )
+ );
+
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
}
- @media (width >= 333px) {
- .sm\:padding-1rem {
- padding: 1rem !important;
+ @include expect() {
+ .opacity-25 {
+ opacity: .25;
}
}
+ }
+ }
+ }
- @media (width >= 666px) {
- .md\:padding-1rem {
- padding: 1rem !important;
- }
+ @include describe("responsive") {
+ @include it("generates responsive classes only for utilities with responsive: true") {
+ $test-utilities: (
+ opacity: (
+ property: opacity,
+ responsive: true,
+ values: (50: .5)
+ ),
+ float: (
+ property: float,
+ values: (start: inline-start)
+ )
+ );
+
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
}
- @media print {
- .print\:font-size-large {
- font-size: 1.25rem !important;
+ @include expect() {
+ .opacity-50 {
+ opacity: .5;
+ }
+
+ .float-start {
+ float: inline-start;
+ }
+
+ @media (width >= 576px) {
+ .sm\:opacity-50 {
+ opacity: .5;
+ }
}
}
}
-
}
- }
- @include it("generates utilities without !important when important: false or not set") {
- $utilities: (
- opacity: (
- property: opacity,
- values: (
- 0: 0,
- 50: .5,
- 100: 1
+ @include it("does not generate responsive classes for utilities without responsive: true") {
+ $test-utilities: (
+ float: (
+ property: float,
+ values: (start: inline-start)
)
- ),
- text-align: (
- property: text-align,
- important: false,
- values: (
- start: left,
- center: center
- )
- )
- ) !global;
+ );
- @include assert() {
- @include output() {
- @import "../../utilities/api";
- }
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
+ }
- @include expect() {
- .opacity-0 {
- opacity: 0;
+ @include expect() {
+ .float-start {
+ float: inline-start;
+ }
+ // nothing at sm — responsive is not set
}
+ }
+ }
+ }
- .opacity-50 {
- opacity: .5;
+ @include describe("dark") {
+ @include it("generates dark-prefixed classes for utilities with dark: true") {
+ $test-utilities: (
+ opacity: (
+ property: opacity,
+ values: (50: .5)
+ ),
+ bg: (
+ property: background-color,
+ class: bg,
+ dark: true,
+ values: (white: #fff, black: #000)
+ )
+ );
+
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
}
- .opacity-100 {
- opacity: 1;
+ @include expect() {
+ .opacity-50 {
+ opacity: .5;
+ }
+
+ .bg-white {
+ background-color: #fff;
+ }
+
+ .bg-black {
+ background-color: #000;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ .dark\:bg-white {
+ background-color: #fff;
+ }
+
+ .dark\:bg-black {
+ background-color: #000;
+ }
+ }
}
+ }
+ }
- .text-align-start {
- text-align: left;
+ @include it("does not generate dark classes for utilities without dark: true") {
+ $test-utilities: (
+ opacity: (
+ property: opacity,
+ values: (50: .5)
+ )
+ );
+
+ @include assert() {
+ @include output() {
+ @include generate-utilities-loop($test-utilities, $test-breakpoints);
}
- .text-align-center {
- text-align: center;
+ @include expect() {
+ .opacity-50 {
+ opacity: .5;
+ }
+ // nothing in @media (prefers-color-scheme: dark) — dark is not set
}
}
}
-@use "sass:map";
-@use "sass:meta";
@use "../config" as *;
-@use "../layout/breakpoints" as *;
@use "../mixins/utilities" as *;
@use "../utilities" as *;
@layer utilities {
- // Loop over each breakpoint
- @each $breakpoint in map.keys($breakpoints) {
-
- // Generate media query if needed
- @include media-breakpoint-up($breakpoint) {
- $prefix: breakpoint-prefix($breakpoint, $breakpoints);
-
- // Loop over each utility property
- @each $key, $utility in $utilities {
- // The utility can be disabled with `false`, thus check if the utility is a map first
- // Only proceed if responsive media queries are enabled or if it's the base media query
- @if meta.type-of($utility) == "map" and (map.get($utility, responsive) or $prefix == "") {
- @include generate-utility($utility, $prefix);
- }
- }
- }
- }
-
- // Print utilities
- @media print {
- @each $key, $utility in $utilities {
- // The utility can be disabled with `false`, thus check if the utility is a map first
- // Then check if the utility needs print styles
- @if meta.type-of($utility) == "map" and map.get($utility, print) == true {
- @include generate-utility($utility, "print\\:");
- }
- }
- }
+ @include generate-utilities-loop($utilities, $breakpoints);
}
const depsCount = deps.length
---
-<div class="bd-page-meta w-100 d-grid md:grid-cols-2 lg:d-flex gap-5 row-gap-2 px-4 py-3 bg-1 fg-2 fs-sm rounded-3">
+<div class="bd-page-meta w-100 d-grid md:grid-cols-2 lg:d-flex gap-5 row-gap-2 px-4 py-3 bg-1 fg-2 fs-sm rounded-5">
{
frontmatter.js === 'required' && (
<div class="d-flex align-items-center gap-2">
Previous major versions have treated Sass as both programmatic and visual customization with CSS variables adding a more complicated, and less complete, secondary layer. This has been greatly improved in v6.
-On top of that, we use [PostCSS](https://postcss.org/) to prefix our CSS variables with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate.
+On top of that, we use [PostCSS](https://postcss.org/) to [prefix our CSS variables](https://github.com/twbs/postcss-prefix-custom-properties) with the `bs-` prefix and add vendor prefixes to our CSS properties, as appropriate.
### Sass
<BsTable class="table table-utilities">
| Option | Type | Default value | Description |
| --- | --- | --- | --- |
-| [`property`](#property) | **Required** | – | Name of the property, this can be a string or an array of strings (e.g., horizontal paddings or margins). |
+| [`property`](#property) | **Required** | – | Name of the property. Can be a string, a space-separated list of strings (e.g., horizontal paddings or margins), or a **map** of property-to-default-value pairs (see [Property-Value Mapping](#property-value-mapping)). |
| [`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. |
| [`responsive`](#responsive) | Optional | `false` | Boolean indicating if responsive classes should be generated. |
| [`important`](#importance) | Optional | `false` | Boolean indicating if `!important` should be added to the utility’s CSS rules. |
| [`print`](#print) | Optional | `false` | Boolean indicating if print classes need to be generated. |
+| [`dark`](#dark) | Optional | `false` | Boolean indicating if `dark:` prefixed classes should be generated, scoped to `@media (prefers-color-scheme: dark)`. |
+| [`enabled`](#enabled) | Optional | `true` | Set to `false` to suppress output for a utility while keeping its definition in the `$utilities` map. |
</BsTable>
## API explained
### `property`
-<div class="badge fs-6 bg-accent mb-3">Required</div>
+<div class="badge badge-subtle theme-danger fs-sm fw-normal align-self-start">Required</div>
The `property` key must be set for any utility, and it must contain a valid CSS property. This property is used in the generated utility’s ruleset. When the `class` key is omitted, it also serves as the default class name. Consider the `text-decoration` utility:
.text-decoration-line-through { text-decoration: line-through; }
```
+#### Property value mapping
+
+The `property` key also accepts a **map** of CSS property names to default values. This lets a single utility class set multiple properties at once, and lets values override individual properties when the value is itself a map.
+
+When a property's default value in the map is `null`, the utility value is used directly for that property. When a default value is set, it is used unless the utility's value map contains a matching key.
+
+A common pattern is pairing a CSS custom property (which receives the utility value) with a regular property that references it:
+
+```scss
+$utilities: (
+ "bg-color": (
+ property: (
+ "--bg": null,
+ "background-color": var(--bg)
+ ),
+ class: bg,
+ values: (
+ primary: var(--blue-500),
+ danger: var(--red-500),
+ )
+ )
+);
+```
+
+Output:
+
+```css
+.bg-primary { --bs-bg: var(--bs-blue-500); background-color: var(--bs-bg); }
+.bg-danger { --bs-bg: var(--bs-red-500); background-color: var(--bs-bg); }
+```
+
+<Callout type="info">
+ Source CSS variables are used without a `--bs-` prefix, which is added in our compiled CSS with [our PostCSS plugin](https://github.com/twbs/postcss-prefix-custom-properties).
+</Callout>
+
+You can also set per-value overrides. When `values` contains a nested map whose keys match properties in the `property` map, those values are used for the matching property:
+
+```scss
+$utilities: (
+ "text-size": (
+ property: (
+ "font-size": 1rem,
+ "line-height": 1.5
+ ),
+ class: text,
+ values: (
+ "sm": ("font-size": .875rem, "line-height": 1.25),
+ "lg": ("font-size": 1.25rem, "line-height": 1.75),
+ )
+ )
+);
+```
+
+Output:
+
+```css
+.text-sm { font-size: .875rem; line-height: 1.25; }
+.text-lg { font-size: 1.25rem; line-height: 1.75; }
+```
+
### `values`
-<div class="badge fs-6 bg-accent mb-3">Required</div>
+<div class="badge badge-subtle theme-danger fs-sm fw-normal align-self-start">Required</div>
Use the `values` key to specify which values for the specified `property` should be used in the generated class names and rules. Can be a list or map (set in the utilities or in a Sass variable).
"link-opacity": (
property: color,
class: link,
- variables: (link-color),
+ variables: link-color,
values: (
10: 10%,
50: 50%,
}
```
+### Dark
+
+Enabling the `dark` option generates `dark:` prefixed classes scoped to `@media (prefers-color-scheme: dark)`. This follows the same prefix convention as responsive and print utilities.
+
+```scss
+$utilities: (
+ "bg-color": (
+ property: background-color,
+ class: bg,
+ dark: true,
+ values: (
+ white: #fff,
+ black: #000,
+ )
+ )
+);
+```
+
+Output:
+
+```css
+.bg-white { background-color: #fff; }
+.bg-black { background-color: #000; }
+
+@media (prefers-color-scheme: dark) {
+ .dark\:bg-white { background-color: #fff; }
+ .dark\:bg-black { background-color: #000; }
+}
+```
+
+### Enabled
+
+Set `enabled: false` to suppress output for a utility without removing it from the `$utilities` map. This is useful when you want to keep the utility's definition available for reference or downstream `map.get()` calls, but don't want it to emit any CSS.
+
+```scss
+$utilities: map.merge(
+ $utilities,
+ (
+ "float": map.merge(
+ map.get($utilities, "float"),
+ ( enabled: false ),
+ ),
+ )
+);
+```
+
## Importance
Utilities generated by the API no longer include `!important` by default in v6. This is because we now use CSS layers to ensure utilities override components and modifier classes as intended. You can enable `!important` on a per-utility basis by setting the `important` option to `true`.
Now that you’re familiar with how the utilities API works, learn how to add your own custom classes and modify our default utilities.
+The examples below use two different import styles depending on the use case:
+
+- **`@use "bootstrap" with (...)`**—Use this when compiling the full Bootstrap framework from a single entrypoint. Configuration is passed via `with (...)` and Bootstrap handles merging in one step.
+- **`@use "bootstrap/scss/utilities" as *` + `@use "bootstrap/scss/utilities/api"`**—Use this for focused, utility-only builds where you need to read from or merge the existing `$utilities` map before passing it to the API layer. Using `as *` avoids a namespace prefix so you can reference `$utilities` directly.
+
### Override utilities
-Override existing utilities by using the same key. For example, if you want additional responsive overflow utility classes, you can do this:
+Override existing utilities by using the same key and passing the new definition via the `$utilities` configuration variable when loading Bootstrap. For example, take our existing overflow utility class definition:
+
+<ScssDocs name="utils-overflow" file="scss/_utilities.scss" />
+
+If you want to make those responsive, and adjust the values used, you can do this:
```scss
-$utilities: (
- "overflow": (
- responsive: true,
- property: overflow,
- values: visible hidden scroll auto,
- ),
+// my-bootstrap.scss
+@use "bootstrap" with (
+ $utilities: (
+ "overflow": (
+ responsive: true,
+ // property: overflow,
+ values: visible hidden scroll auto,
+ ),
+ )
);
```
### Add utilities
-New utilities can be added to the default `$utilities` map with a `map.merge`. Make sure our required Sass files and `_utilities.scss` are loaded first, then use `map.merge` to add your additional utilities. For example, here’s how to add a responsive `cursor` utility with three values.
+New utilities can be added to the default `$utilities` map by passing them via the `$utilities` configuration variable when loading Bootstrap. Bootstrap merges your map on top of its defaults, so only supply the new utilities you want to add.
```scss
-@use "bootstrap/scss/bootstrap" as *;
-
-$utilities: map.merge(
- $utilities,
- (
+// my-bootstrap.scss
+@use "bootstrap" with (
+ $utilities: (
"cursor": (
property: cursor,
class: cursor,
)
)
);
-
-@use "bootstrap/scss/utilities/api";
```
### Modify utilities
-Modify existing utilities in the default `$utilities` map with `map.get` and `map.merge` functions. In the example below, we’re adding an additional value to the `width` utilities. Start with an initial `map.merge` and then specify which utility you want to modify. From there, fetch the nested `"width"` map with `map.get` to access and modify the utility’s options and values.
+Modify existing utilities by passing a modified copy of the utility via the `$utilities` configuration variable. Use `map.get` and `map.merge` to build the new value from the existing definition. In the example below, we’re adding an additional value to the `width` utilities:
```scss
-@use "bootstrap/scss/bootstrap" as *;
+// my-bootstrap.scss
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.merge(
$utilities,
)
);
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
#### Enable responsive
You can enable responsive classes for an existing set of utilities that are not currently responsive by default. For example, to make the `border` classes responsive:
```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.merge(
$utilities,
)
);
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
This will now generate responsive variations of `.border` and `.border-0` for each breakpoint. Your generated CSS will look like this:
Missing v4 utilities, or used to another naming convention? The utilities API can be used to override the resulting `class` of a given utility—for example, to rename `.ms-*` utilities to oldish `.ml-*`:
```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.merge(
$utilities,
)
);
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
### Remove utilities
Remove any of the default utilities with the [`map.remove()` Sass function](https://sass-lang.com/documentation/modules/map/#remove).
```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.remove($utilities, "width", "float");
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
You can also use the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge) and set the group key to `null` to remove the utility.
```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.merge(
$utilities,
)
);
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
### Add, remove, modify
You can add, remove, and modify many utilities all at once with the [`map.merge()` Sass function](https://sass-lang.com/documentation/modules/map/#merge). Here’s how you can combine the previous examples into one larger map.
```scss
-@use "bootstrap/scss/bootstrap" as *;
+@use "sass:map";
+@use "bootstrap/scss/utilities" as *;
$utilities: map.merge(
$utilities,
)
);
-@use "bootstrap/scss/utilities/api";
+@use "bootstrap/scss/utilities/api" with ($utilities: $utilities);
```
<Placeholder width="75" height="75" class="rounded-start-pill" title="Example left rounded pill image" />
<Placeholder width="75" height="75" class="rounded-5 rounded-top-0" title="Example extra large bottom rounded image" />`} />
+## Mixing
+
+Use `.rounded-size-{n}` to set the radius size without rounding all corners. Combine it with side classes like `.rounded-top` or `.rounded-end` to round only specific sides at that size. Because these use CSS variables for sizing, you may need to apply additional classes to nested elements to prevent inheritance.
+
+<Example class="bd-example-rounded-utils" code={`<Placeholder width="75" height="75" class="rounded-size-6 rounded-top" title="Top corners only, size 6" />
+<Placeholder width="75" height="75" class="rounded-size-6 rounded-top rounded-end" title="Top and end corners, size 6" />
+<Placeholder width="75" height="75" class="rounded-size-3 rounded-top rounded-bottom" title="Top and bottom, size 3" />
+<Placeholder width="75" height="75" class="rounded-size-pill rounded-top" title="Top corners only, pill" />`} />
+
+When `.rounded-{n}` is used alongside a side class, the number sets the `--rounded-size` variable that the side class inherits.
+
+<Example class="bd-example-rounded-utils" code={`<Placeholder width="75" height="75" class="rounded-6 rounded-bottom-0" title="All corners size 6, bottom corners removed" />
+<Placeholder width="75" height="75" class="rounded-3 rounded-top" title="All corners size 3, top corners stay size 3 via --rounded-size" />`} />
+
## CSS
### Variables
<ScssDocs name="root-spacer-loop" file="scss/_root.scss" />
+The `--rounded-size` local CSS variable is set by `.rounded-{n}` and `.rounded-size-{n}` and consumed by the side utilities (`.rounded-top`, `.rounded-end`, `.rounded-bottom`, `.rounded-start`) as their default radius. You can also set it directly in your own CSS or via an inline style to control the side utilities from a single point.
+
### Sass maps
### Sass mixins
Text wrapping 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
-"white-space": (
- property: white-space,
- class: text,
- values: (
- wrap: normal,
- nowrap: nowrap,
- )
-),
-"word-wrap": (
- property: word-wrap word-break,
- class: text,
- values: (break: break-word),
-),
-```
+<ScssDocs name="utils-text-wrap" file="scss/_utilities.scss" />
+
+<ScssDocs name="utils-text-break" file="scss/_utilities.scss" />