From: Mark Otto Date: Fri, 5 Jun 2026 03:09:03 +0000 (-0700) Subject: Move SVGs to CSS masks (#42465) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dd8fe12dba8524f02821694c2d858315b5e2f873;p=thirdparty%2Fbootstrap.git Move SVGs to CSS masks (#42465) * Move SVGs to CSS masks * update migration, remove unused, fix a couple things * Format * Bump bundlewatch * Properly size btn-close with mask * Apply .check class directly to checkbox input, drop wrapper The .check wrapper existed only to host the checkmark pseudo-element. Since appearance: none controls render pseudo-elements (as .radio already relies on), move .check onto the itself and draw the checked/ indeterminate mark via a masked ::before. Removes the grid/:has machinery and makes checkbox and radio markup symmetric. * Use .form-field for list group radio example, matching checkboxes --- diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index b6ec56b0e3..25c981393a 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -26,11 +26,11 @@ }, { "path": "./dist/css/bootstrap.css", - "maxSize": "48.75 kB" + "maxSize": "49.25 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "45.25 kB" + "maxSize": "46.0 kB" }, { "path": "./dist/js/bootstrap.bundle.js", diff --git a/scss/_breadcrumb.scss b/scss/_breadcrumb.scss index 499d352310..03f4240cf4 100644 --- a/scss/_breadcrumb.scss +++ b/scss/_breadcrumb.scss @@ -1,5 +1,6 @@ @use "functions" as *; @use "mixins/border-radius" as *; +@use "mixins/mask-icon" as *; @use "mixins/transition" as *; @use "mixins/tokens" as *; @@ -14,6 +15,9 @@ $breadcrumb-tokens: defaults( --breadcrumb-bg: transparent, --breadcrumb-border-radius: var(--radius-5), --breadcrumb-divider-color: var(--fg-4), + --breadcrumb-divider-icon: #{escape-svg(url("data:image/svg+xml,"))}, + --breadcrumb-divider-width: .375rem, + --breadcrumb-divider-height: .75rem, --breadcrumb-link-padding-x: .75rem, --breadcrumb-link-padding-y: .25rem, --breadcrumb-link-color: var(--fg-3), @@ -47,6 +51,18 @@ $breadcrumb-tokens: defaults( .breadcrumb-divider { margin-inline: calc(var(--breadcrumb-link-padding-x) / 4); color: var(--breadcrumb-divider-color); + + // Render a default chevron, painted with `currentcolor` via a mask, when the + // divider has no explicit content. Any content (an inline SVG, a text + // character, etc.) added to the element overrides this default. + &:empty::before { + display: block; + width: var(--breadcrumb-divider-width); + height: var(--breadcrumb-divider-height); + content: ""; + background-color: currentcolor; + @include mask-icon(var(--breadcrumb-divider-icon)); + } } .breadcrumb-link { diff --git a/scss/_carousel.scss b/scss/_carousel.scss index 10bfdbad0c..550df2fc31 100644 --- a/scss/_carousel.scss +++ b/scss/_carousel.scss @@ -3,6 +3,7 @@ @use "functions" as *; @use "mixins/transition" as *; @use "mixins/color-mode" as *; +@use "mixins/mask-icon" as *; @use "mixins/tokens" as *; $carousel-tokens: () !default; @@ -30,8 +31,8 @@ $carousel-tokens: defaults( --carousel-caption-padding-y: 1.25rem, --carousel-caption-spacer: 1.25rem, --carousel-control-icon-width: 2rem, - --carousel-control-prev-icon-bg: url("data:image/svg+xml,"), - --carousel-control-next-icon-bg: url("data:image/svg+xml,"), + --carousel-control-prev-icon: url("data:image/svg+xml,"), + --carousel-control-next-icon: url("data:image/svg+xml,"), --carousel-transition-duration: .6s, --carousel-transition: transform .6s ease-in-out, ), @@ -178,31 +179,30 @@ $carousel-dark-tokens: defaults( background-image: if(sass($enable-gradients): linear-gradient(270deg, rgb(0 0 0 / .25), rgb(0 0 0 / .001)); else: null); } - // Icons for within + // Icons for within, rendered via CSS mask so they inherit a configurable color .carousel-control-prev-icon, .carousel-control-next-icon { display: inline-block; width: var(--carousel-control-icon-width); height: var(--carousel-control-icon-width); - background-repeat: no-repeat; - background-position: 50%; - background-size: 100% 100%; + background-color: var(--carousel-control-color); + @include mask-icon($size: 100% 100%, $position: 50%); } .carousel-control-prev-icon { - background-image: var(--carousel-control-prev-icon-bg); + mask-image: var(--carousel-control-prev-icon); } [dir="rtl"] .carousel-control-prev-icon { - background-image: var(--carousel-control-next-icon-bg); + mask-image: var(--carousel-control-next-icon); } .carousel-control-next-icon { - background-image: var(--carousel-control-next-icon-bg); + mask-image: var(--carousel-control-next-icon); } [dir="rtl"] .carousel-control-next-icon { - background-image: var(--carousel-control-prev-icon-bg); + mask-image: var(--carousel-control-prev-icon); } // Optional indicator pips/controls diff --git a/scss/_datepicker.scss b/scss/_datepicker.scss index ae8c9adec6..d45f1e39a5 100644 --- a/scss/_datepicker.scss +++ b/scss/_datepicker.scss @@ -4,6 +4,7 @@ @use "config" as *; @use "mixins/border-radius" as *; @use "mixins/focus-ring" as *; +@use "mixins/mask-icon" as *; @use "mixins/tokens" as *; $datepicker-tokens: () !default; @@ -113,9 +114,8 @@ $datepicker-tokens: defaults( position: absolute; inset: .25rem; content: ""; - background-image: url("data:image/svg+xml,"); - background-repeat: no-repeat; - background-position: center; + background-color: var(--datepicker-color); + @include mask-icon(url("data:image/svg+xml,"), $size: null); } &:hover { diff --git a/scss/_drawer.scss b/scss/_drawer.scss index 7c9edeac83..b14f175694 100644 --- a/scss/_drawer.scss +++ b/scss/_drawer.scss @@ -266,11 +266,8 @@ $drawer-backdrop-tokens: defaults( @include dialog-header(var(--drawer-padding-y) var(--drawer-padding-x)); .btn-close { - padding: calc(var(--drawer-padding-y) * .5) calc(var(--drawer-padding-x) * .5); + margin-block: calc(-.5 * var(--drawer-padding-y)); margin-inline-start: auto; - margin-inline-end: calc(-.5 * var(--drawer-padding-x)); - margin-top: calc(-.5 * var(--drawer-padding-y)); - margin-bottom: calc(-.5 * var(--drawer-padding-y)); } } diff --git a/scss/_navbar.scss b/scss/_navbar.scss index 7c30a57a20..441a08aa5a 100644 --- a/scss/_navbar.scss +++ b/scss/_navbar.scss @@ -2,6 +2,7 @@ @use "functions" as *; @use "layout/breakpoints" as *; @use "mixins/box-shadow" as *; +@use "mixins/mask-icon" as *; @use "mixins/tokens" as *; @use "mixins/transition" as *; @@ -40,6 +41,8 @@ $navbar-tokens: defaults( --navbar-toggler-border-color: color-mix(in oklch, var(--fg-body) 15%, transparent), --navbar-toggler-border-radius: var(--radius-5), --navbar-toggler-transition: box-shadow .15s ease-in-out, + --navbar-toggler-icon-size: 1.25rem, + --navbar-toggler-icon: #{escape-svg(url("data:image/svg+xml,"))}, ), $navbar-tokens ); @@ -182,6 +185,15 @@ $navbar-nav-tokens: defaults( --btn-hover-bg: var(--bg-2); } + // Hamburger icon, rendered via CSS mask so it inherits the navbar color + .navbar-toggler-icon { + display: inline-block; + width: var(--navbar-toggler-icon-size); + height: var(--navbar-toggler-icon-size); + background-color: currentcolor; + @include mask-icon(var(--navbar-toggler-icon)); + } + // scss-docs-start navbar-expand-loop // Generate series of responsive `.navbar-expand` classes for configuring // where your navbar collapses and expands. Uses container queries so the diff --git a/scss/buttons/_close.scss b/scss/buttons/_close.scss index 98c653eba0..02a392326a 100644 --- a/scss/buttons/_close.scss +++ b/scss/buttons/_close.scss @@ -1,6 +1,7 @@ @use "../functions" as *; @use "../mixins/border-radius" as *; @use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; @use "../mixins/tokens" as *; $btn-close-tokens: () !default; @@ -9,8 +10,9 @@ $btn-close-tokens: () !default; // stylelint-disable-next-line scss/dollar-variable-default $btn-close-tokens: defaults( ( - --btn-close-size: 1.25rem, + --btn-close-size: 1.5rem, --btn-close-color: inherit, + --btn-close-icon: #{escape-svg(url("data:image/svg+xml,"))}, --btn-close-opacity: .5, --btn-close-hover-opacity: .75, --btn-close-focus-opacity: .85, @@ -33,17 +35,11 @@ $btn-close-tokens: defaults( height: var(--btn-close-size); padding: 0; color: var(--btn-close-color); - background: transparent; // for button elements + background-color: currentcolor; border: 0; // for button elements @include border-radius(var(--radius-5)); opacity: var(--btn-close-opacity); - - > svg { - display: block; - width: 100%; - height: 100%; - fill: currentcolor; - } + @include mask-icon(var(--btn-close-icon)); // Override 's hover style &:hover { diff --git a/scss/forms/_check.scss b/scss/forms/_check.scss index 2e83bbc255..6264402c59 100644 --- a/scss/forms/_check.scss +++ b/scss/forms/_check.scss @@ -1,5 +1,6 @@ @use "../functions" as *; @use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; @use "../mixins/tokens" as *; // stylelint-disable custom-property-no-missing-var-function @@ -14,6 +15,8 @@ $check-tokens: defaults( --check-bg: var(--bg-body), --check-border-color: var(--border-color), --check-border-radius: .375rem, + --check-icon-checked: #{escape-svg(url("data:image/svg+xml,"))}, + --check-icon-indeterminate: #{escape-svg(url("data:image/svg+xml,"))}, --check-checked-bg: var(--control-checked-bg), --check-checked-border-color: var(--control-checked-border-color), --check-indeterminate-bg: var(--control-checked-bg), @@ -29,48 +32,49 @@ $check-tokens: defaults( // stylelint-enable custom-property-no-missing-var-function @layer forms { + // The class lives on the `` itself; `appearance: none` controls render + // pseudo-elements, so the mark is drawn directly on the input — no wrapper. .check { @include tokens($check-tokens); - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); + position: relative; + flex-shrink: 0; + width: var(--check-size); + height: var(--check-size); margin-block: var(--check-margin-block); + appearance: none; + // later: maybe set a tertiary bg color? + background-color: var(--theme-bg, var(--check-bg)); + border: 1px solid var(--theme-bg, var(--check-border-color)); + // stylelint-disable-next-line property-disallowed-list + border-radius: .3em; - :where(svg, input) { - flex-shrink: 0; - grid-row-start: 1; - grid-column-start: 1; - width: var(--check-size); - height: var(--check-size); - } - - :where(input) { - appearance: none; - // later: maybe set a tertiary bg color? - background-color: var(--theme-bg, var(--check-bg)); - border: 1px solid var(--theme-bg, var(--check-border-color)); - // stylelint-disable-next-line property-disallowed-list - border-radius: .3em; - } - - :where(input:checked, input:indeterminate) { + &:checked, + &:indeterminate { background-color: var(--theme-bg, var(--check-checked-bg)); border-color: var(--theme-bg, var(--check-checked-border-color)); + + // Check/indeterminate mark, overlaid on the input and rendered via a CSS + // mask so it inherits the contrast color without an inline SVG. + &::before { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + background-color: var(--theme-contrast, var(--primary-contrast)); + @include mask-icon(); + } } - :where(input:focus-visible) { + &:checked::before { mask-image: var(--check-icon-checked); } + &:indeterminate::before { mask-image: var(--check-icon-indeterminate); } + + &:focus-visible { @include focus-ring(true); --focus-ring-offset: -1px; } - &:has(input:checked) .checked, - &:has(input:indeterminate) .indeterminate { - display: block; - color: var(--theme-contrast, var(--primary-contrast)); - stroke: currentcolor; - } - - &:has(input:disabled) { + &:disabled { --check-bg: var(--check-disabled-bg); ~ label { @@ -78,17 +82,9 @@ $check-tokens: defaults( cursor: default; } } - &:has(input:disabled:checked) { + &:disabled:checked { opacity: var(--check-disabled-opacity); } - - :where(svg) { - pointer-events: none; - } - - :where(svg path) { - display: none; - } } .check-sm { diff --git a/scss/forms/_validation.scss b/scss/forms/_validation.scss index adc50d2045..58660f3cc8 100644 --- a/scss/forms/_validation.scss +++ b/scss/forms/_validation.scss @@ -70,7 +70,7 @@ $validation-states: defaults( } // Checkbox — control-level styling (border, checked bg, focus ring). - .check input { + .check { @include form-validation-state-selector($state) { --check-border-color: var(--#{$theme}-border); --check-checked-bg: var(--#{$theme}-bg); @@ -83,7 +83,7 @@ $validation-states: defaults( } // Checkbox — label color and feedback display via .form-field:has(). - .form-field:has(.check input.is-#{$state}) { + .form-field:has(.check.is-#{$state}) { label { color: var(--#{$theme}-fg); } .#{$state}-feedback, @@ -91,14 +91,14 @@ $validation-states: defaults( } @if $state == "invalid" { - [data-bs-validate] .form-field:has(.check input:user-invalid) { + [data-bs-validate] .form-field:has(.check:user-invalid) { label { color: var(--#{$theme}-fg); } .invalid-feedback, .invalid-tooltip { display: block; } } } @else if $state == "valid" { - [data-bs-validate~="valid"] .form-field:has(.check input:user-valid) { + [data-bs-validate~="valid"] .form-field:has(.check:user-valid) { label { color: var(--#{$theme}-fg); } .valid-feedback, diff --git a/scss/mixins/_mask-icon.scss b/scss/mixins/_mask-icon.scss new file mode 100644 index 0000000000..f4d1fd7f12 --- /dev/null +++ b/scss/mixins/_mask-icon.scss @@ -0,0 +1,21 @@ +// Mask icon +// +// Renders an SVG icon via a CSS mask so the shape is painted with the +// element's `background-color` and therefore inherits theme/dark-mode color. +// Set `background-color` on the element itself; pass `null` for `$icon` when +// the mask image is applied conditionally (e.g. per state or direction). + +// scss-docs-start mask-icon-mixin +@mixin mask-icon($icon: null, $size: contain, $position: center) { + @if $icon != null { + mask-image: $icon; + } + mask-repeat: no-repeat; + @if $position != null { + mask-position: $position; + } + @if $size != null { + mask-size: $size; + } +} +// scss-docs-end mask-icon-mixin diff --git a/scss/mixins/index.scss b/scss/mixins/index.scss index 881ee9176e..a0201ed347 100644 --- a/scss/mixins/index.scss +++ b/scss/mixins/index.scss @@ -23,6 +23,7 @@ @forward "backdrop"; @forward "caret"; @forward "form-validation"; +@forward "mask-icon"; // Skins @forward "border-radius"; diff --git a/site/src/assets/examples/album/index.astro b/site/src/assets/examples/album/index.astro index a670e5658e..96867b7515 100644 --- a/site/src/assets/examples/album/index.astro +++ b/site/src/assets/examples/album/index.astro @@ -31,7 +31,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" Album diff --git a/site/src/assets/examples/carousel/index.astro b/site/src/assets/examples/carousel/index.astro index c21400bd0a..a98848b289 100644 --- a/site/src/assets/examples/carousel/index.astro +++ b/site/src/assets/examples/carousel/index.astro @@ -9,7 +9,7 @@ import Placeholder from "@shortcodes/Placeholder.astro"
Carousel
-
- - - - - -
+
@@ -392,13 +386,7 @@ export const body_class = 'bg-body-tertiary'
-
- - - - - -
+ @@ -587,13 +575,7 @@ export const body_class = 'bg-body-tertiary'
-
- - - - - -
+ @@ -1210,7 +1192,7 @@ export const body_class = 'bg-body-tertiary' Bootstrap