From 7c2b4d035cd309ead44bbc92abf9a5d76748e8e3 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 30 Jun 2026 15:36:40 -0700 Subject: [PATCH 01/10] Add keyline (.5px) border width --- scss/_config.scss | 1 + scss/_root.scss | 1 + scss/_utilities.scss | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scss/_config.scss b/scss/_config.scss index 835db327215d..ada6bfd48d14 100644 --- a/scss/_config.scss +++ b/scss/_config.scss @@ -203,6 +203,7 @@ $paragraph-margin-bottom: 1rem !default; // scss-docs-start border-variables $border-width: 1px !default; $border-widths: ( + keyline: .5px, 1: 1px, 2: 2px, 3: 3px, diff --git a/scss/_root.scss b/scss/_root.scss index 3d53bbd0340c..0d0dd1af9af6 100644 --- a/scss/_root.scss +++ b/scss/_root.scss @@ -49,6 +49,7 @@ $root-tokens: defaults( // scss-docs-start root-border-var --border-width: #{$border-width}, + --border-width-keyline: .5px, --border-style: #{$border-style}, --border-color: light-dark(var(--gray-200), var(--gray-700)), --border-color-translucent: color-mix(in oklch, var(--fg-body) 15%, transparent), diff --git a/scss/_utilities.scss b/scss/_utilities.scss index 96aa6393105e..2da999aafeaa 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -225,7 +225,7 @@ $utilities: map.merge( "border-width": ( property: border-width, class: border, - values: $border-widths + values: map.merge($border-widths, (keyline: var(--border-width-keyline))) ), "border-opacity": ( class: border, From 06a586ca418a41baec3384d5338b6230190887e0 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 30 Jun 2026 15:36:47 -0700 Subject: [PATCH 02/10] Add layered shadow system with color, opacity, and strength tokens --- scss/_dialog.scss | 2 +- scss/_drawer.scss | 2 +- scss/_menu.scss | 2 +- scss/_root.scss | 46 +++++++++++++++++++++++++++++++++++++------- scss/_utilities.scss | 45 ++++++++++++++++++++++++++++++++++++++----- 5 files changed, 82 insertions(+), 15 deletions(-) diff --git a/scss/_dialog.scss b/scss/_dialog.scss index 69bada97b145..0e9cf1975d67 100644 --- a/scss/_dialog.scss +++ b/scss/_dialog.scss @@ -27,7 +27,7 @@ $dialog-tokens: defaults( --dialog-border-color: var(--border-color-translucent), --dialog-border-width: var(--border-width), --dialog-border-radius: var(--radius-7), - --dialog-box-shadow: var(--box-shadow-lg), + --dialog-box-shadow: var(--box-shadow-xl), --dialog-transition-duration: .3s, --dialog-transition-timing: cubic-bezier(.22, 1, .36, 1), --dialog-backdrop-bg: light-dark(rgb(0 0 0 / 50%), rgb(0 0 0 / 65%)), diff --git a/scss/_drawer.scss b/scss/_drawer.scss index d26bcb793e95..ac45e076d6fb 100644 --- a/scss/_drawer.scss +++ b/scss/_drawer.scss @@ -26,7 +26,7 @@ $drawer-tokens: defaults( --drawer-border-width: var(--border-width), --drawer-border-color: var(--border-color-translucent), --drawer-border-radius: var(--radius-7), - --drawer-box-shadow: var(--box-shadow-lg), + --drawer-box-shadow: var(--box-shadow-xl), --drawer-transition-duration: .3s, --drawer-transition-timing: cubic-bezier(.22, 1, .36, 1), --drawer-title-line-height: 1.5, diff --git a/scss/_menu.scss b/scss/_menu.scss index cddc5ab7de46..31c7853d328e 100644 --- a/scss/_menu.scss +++ b/scss/_menu.scss @@ -23,7 +23,7 @@ $menu-tokens: defaults( // --menu-border-color: var(--border-color-translucent), // --menu-border-radius: var(--radius-7), // --menu-border-width: var(--border-width), - --menu-box-shadow: var(--box-shadow), + --menu-box-shadow: var(--box-shadow-lg), // --menu-max-height: none, --menu-divider-bg: var(--border-color-translucent), --menu-divider-margin-y: .125rem, diff --git a/scss/_root.scss b/scss/_root.scss index 0d0dd1af9af6..e7d675b73a2e 100644 --- a/scss/_root.scss +++ b/scss/_root.scss @@ -10,7 +10,7 @@ $root-tokens: () !default; // scss-docs-start root-tokens -// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines, custom-property-no-missing-var-function // stylelint-disable-next-line scss/dollar-variable-default $root-tokens: defaults( ( @@ -56,11 +56,33 @@ $root-tokens: defaults( // scss-docs-end root-border-var // scss-docs-start root-box-shadow-variables - --box-shadow-xs: 0 .0625rem .1875rem rgb(0 0 0 / 7.5%), - --box-shadow-sm: 0 .125rem .25rem rgb(0 0 0 / 7.5%), - --box-shadow: 0 .5rem 1rem rgb(0 0 0 / 15%), - --box-shadow-lg: 0 1rem 3rem rgb(0 0 0 / 17.5%), - --box-shadow-inset: inset 0 1px 2px rgb(0 0 0 / 7.5%), + // Each layer's alpha is base * strength * opacity, so the shadow-color token + // retints (e.g. .shadow-primary), shadow-opacity dims or boosts + // (.shadow-opacity-*), and shadow-strength deepens every shadow in dark mode. + --shadow-color: #000, + --shadow-strength: 1, + --box-shadow-sm: #{( + "0 .0625rem .125rem oklch(from var(--shadow-color) l c h / calc(.04 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .125rem .375rem -.0625rem oklch(from var(--shadow-color) l c h / calc(.05 * var(--shadow-strength) * var(--shadow-opacity, 1)))" + )}, + --box-shadow: #{( + "0 .125rem .25rem -.0625rem oklch(from var(--shadow-color) l c h / calc(.04 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .375rem .75rem -.125rem oklch(from var(--shadow-color) l c h / calc(.05 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .75rem 1.5rem -.25rem oklch(from var(--shadow-color) l c h / calc(.06 * var(--shadow-strength) * var(--shadow-opacity, 1)))" + )}, + --box-shadow-lg: #{( + "0 .125rem .375rem -.0625rem oklch(from var(--shadow-color) l c h / calc(.05 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .375rem 1rem -.1875rem oklch(from var(--shadow-color) l c h / calc(.06 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .75rem 2rem -.375rem oklch(from var(--shadow-color) l c h / calc(.07 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 1.5rem 3.5rem -.75rem oklch(from var(--shadow-color) l c h / calc(.08 * var(--shadow-strength) * var(--shadow-opacity, 1)))" + )}, + --box-shadow-xl: #{( + "0 .1875rem .5rem -.0625rem oklch(from var(--shadow-color) l c h / calc(.05 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 .5rem 1.25rem -.1875rem oklch(from var(--shadow-color) l c h / calc(.06 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 1rem 2.5rem -.375rem oklch(from var(--shadow-color) l c h / calc(.07 * var(--shadow-strength) * var(--shadow-opacity, 1)))", + "0 2rem 5rem -.75rem oklch(from var(--shadow-color) l c h / calc(.09 * var(--shadow-strength) * var(--shadow-opacity, 1)))" + )}, + --box-shadow-inset: inset 0 1px 2px light-dark(oklch(from var(--shadow-color) l c h / calc(8% * var(--shadow-opacity, 1))), oklch(from var(--shadow-color) l c h / calc(26% * var(--shadow-opacity, 1)))), // scss-docs-end root-box-shadow-variables --spacer: 1rem, @@ -114,7 +136,7 @@ $root-tokens: defaults( ), $root-tokens ); -// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines, custom-property-no-missing-var-function // scss-docs-end root-tokens // scss-docs-start root-font-size-loop @@ -179,10 +201,20 @@ $root-tokens: map.set($root-tokens, --radius-pill, 50rem); scrollbar-gutter: stable; } +// Deepen shadows in dark mode. The shadow-strength token is a plain number, so it +// can't use light-dark() (colors only); vary it alongside color-scheme instead. +@media (prefers-color-scheme: dark) { + :root { + --shadow-strength: 2.4; + } +} + [data-bs-theme="dark"] { + --shadow-strength: 2.4; color-scheme: dark; } [data-bs-theme="light"] { + --shadow-strength: 1; color-scheme: light; } diff --git a/scss/_utilities.scss b/scss/_utilities.scss index 2da999aafeaa..ca2775f4e0ac 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -1,6 +1,7 @@ @use "sass:map"; @use "config" as *; @use "functions" as *; +@use "root" as *; @use "theme" as *; // add: @@ -9,6 +10,14 @@ // update: // - focus-ring if needed +// Shadow opacity acts as a midpoint: double the standard opacity scale so +// .shadow-opacity-50 → 1 (== the default shadow) and .shadow-opacity-100 → 2 +// (twice as strong). The shadow expressions read a clean var(--shadow-opacity, 1). +$shadow-opacities: () !default; +@each $key, $value in $util-opacity { + $shadow-opacities: map.set($shadow-opacities, $key, $value * 2); +} + $utilities: () !default; // stylelint-disable-next-line scss/dollar-variable-default $utilities: map.merge( @@ -106,15 +115,41 @@ $utilities: map.merge( "shadow": ( property: box-shadow, class: shadow, - values: ( - null: var(--box-shadow), - xs: var(--box-shadow-xs), - sm: var(--box-shadow-sm), - lg: var(--box-shadow-lg), + // Pull the literal layered expressions straight from the :root tokens + // (single source of truth in scss/_root.scss). Emitting the literal—rather + // than var(--box-shadow-*)—keeps the nested var(--shadow-color) / + // var(--shadow-opacity) unresolved so they resolve on the element, letting + // .shadow-{color} and .shadow-opacity-* take effect. + values: ( + null: map.get($root-tokens, --box-shadow), + sm: map.get($root-tokens, --box-shadow-sm), + lg: map.get($root-tokens, --box-shadow-lg), + xl: map.get($root-tokens, --box-shadow-xl), none: none, ) ), // scss-docs-end utils-shadow + // scss-docs-start utils-shadow-color + "shadow-color": ( + property: --shadow-color, + class: shadow, + values: map-merge-multiple( + theme-color-refs("base"), + ( + "current": currentcolor, + "black": var(--black), + "white": var(--white), + ) + ) + ), + // scss-docs-end utils-shadow-color + // scss-docs-start utils-shadow-opacity + "shadow-opacity": ( + property: --shadow-opacity, + class: shadow-opacity, + values: $shadow-opacities + ), + // scss-docs-end utils-shadow-opacity // scss-docs-start utils-position "position": ( property: position, From f4e2fea55d66abfa81c24e1013a039c34b95b69b Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 30 Jun 2026 15:36:51 -0700 Subject: [PATCH 03/10] Docs: document shadow color, opacity, and the new .shadow-xl size --- site/src/content/docs/utilities/shadows.mdx | 38 ++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/site/src/content/docs/utilities/shadows.mdx b/site/src/content/docs/utilities/shadows.mdx index 5f3abbba0939..c3c1fa1fd39b 100644 --- a/site/src/content/docs/utilities/shadows.mdx +++ b/site/src/content/docs/utilities/shadows.mdx @@ -10,10 +10,36 @@ utility: While shadows on components are disabled by default in Bootstrap and can be enabled via `$enable-shadows`, you can also quickly add or remove a shadow with our `box-shadow` utility classes. Includes support for `.shadow-none` and three default sizes (which have associated variables to match). -No shadow -
Small shadow
-
Regular shadow
-
Larger shadow
`} /> +Each size is a layered, multi-stop `box-shadow` for a smoother, more natural falloff, and every layer derives its color from a single `--shadow-color` token—so you can retint shadows in one place. A global `--shadow-strength` token (`light-dark(1, 2.4)`) deepens every shadow in dark mode and doubles as a knob to scale all shadows at once. + +No shadow +
Small shadow
+
Regular shadow
+
Larger shadow
+
Extra large shadow
`} /> + +### Shadow color + +Change the color of a shadow with `.shadow-{color}` utilities for any theme color, plus `.shadow-current` (matches the element’s `color`), `.shadow-black`, and `.shadow-white`. Pair a color utility with a size utility. Colored shadows are intentionally subtle—use a larger size like `.shadow-xl` for a more visible tint. + +Primary +
Success
+
Danger
+
Black
`} /> + +### Shadow opacity + +Scale a shadow’s strength with `.shadow-opacity-{10-100}` utilities. Opacity is treated as a midpoint, not a ceiling: the default shadow matches `.shadow-opacity-50`, so lower values lighten it while higher values—up to `.shadow-opacity-100`—deepen it. This works with any size or color utility. + +20% +
50% (default)
+
100%
`} /> + +Even works with custom shadow colors: + +20% +
50% (default)
+
100%
`} /> ## CSS @@ -26,3 +52,7 @@ While shadows on components are disabled by default in Bootstrap and can be enab Shadow utilities are declared in our utilities API in `scss/_utilities.scss`. [Learn how to use the utilities API.]([[docsref:/utilities/api#using-the-api]]) + + + + From a306345c65a999f59c391d580450e969cc34bc45 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 30 Jun 2026 15:36:54 -0700 Subject: [PATCH 04/10] Docs: add keyline example to border width utilities --- site/src/content/docs/utilities/border.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/content/docs/utilities/border.mdx b/site/src/content/docs/utilities/border.mdx index 12a35b540ff5..08af9d77bec2 100644 --- a/site/src/content/docs/utilities/border.mdx +++ b/site/src/content/docs/utilities/border.mdx @@ -42,7 +42,8 @@ Or remove borders: Border width utilities are available in increments of 1px from 0 to 5px. - + + From 53a06aa3744ce6ceed65183ab40db61cb86e1b1d Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 30 Jun 2026 15:36:59 -0700 Subject: [PATCH 05/10] Docs: use keyline borders on header menus and search input --- site/src/components/header/Navigation.astro | 2 +- site/src/components/header/Versions.astro | 2 +- site/src/layouts/partials/ThemeToggler.astro | 2 +- site/src/scss/_search.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/header/Navigation.astro b/site/src/components/header/Navigation.astro index 9bc8c037ae5d..165af1caf1c9 100644 --- a/site/src/components/header/Navigation.astro +++ b/site/src/components/header/Navigation.astro @@ -45,7 +45,7 @@ const activeSection = !layout ? 'Home' : title === 'Examples' ? 'Examples' : 'Do {activeSection} - `} /> ### Shadow opacity -Scale a shadow’s strength with `.shadow-opacity-{10-100}` utilities. Opacity is treated as a midpoint, not a ceiling: the default shadow matches `.shadow-opacity-50`, so lower values lighten it while higher values—up to `.shadow-opacity-100`—deepen it. This works with any size or color utility. +Scale a shadow’s strength with `.shadow-opacity-{10-100}` utilities. Opacity is treated as a midpoint, not a ceiling: the default shadow matches `.shadow-opacity-50`, so lower values lighten it while higher values—up to `.shadow-opacity-100`—deepen it. This works with any size or color utility. Like `--bs-sc`, `--bs-so` is scoped to the element, so pair it with a size or color utility on the same element rather than expecting it to inherit. -20% -
50% (default)
-
100%
`} /> +20% +
50% (default)
+
100%
`} /> Even works with custom shadow colors: -20% -
50% (default)
-
100%
`} /> +20% +
50% (default)
+
100%
`} /> ## CSS @@ -47,6 +64,8 @@ Even works with custom shadow colors: + + ### Sass utilities API Shadow utilities are declared in our utilities API in `scss/_utilities.scss`. [Learn how to use the utilities API.]([[docsref:/utilities/api#using-the-api]]) From 13bde57ccd1724a2e68cc50888ea0ecf8695ef2d Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Wed, 15 Jul 2026 15:33:48 -0700 Subject: [PATCH 09/10] format --- site/src/content/docs/utilities/shadows.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/content/docs/utilities/shadows.mdx b/site/src/content/docs/utilities/shadows.mdx index cae8442abb74..7a4c7a7bc0fb 100644 --- a/site/src/content/docs/utilities/shadows.mdx +++ b/site/src/content/docs/utilities/shadows.mdx @@ -41,7 +41,9 @@ Because shadow color and opacity utilities set the non-inheriting `--bs-sc` and Parent with .shadow-primary -
Nested .shadow-xl keeps the default color
+
+ Nested .shadow-xl keeps the default color. +
`} /> ### Shadow opacity From 705fce59582759a79e33edec11e87afb853a7890 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 16 Jul 2026 19:50:05 -0700 Subject: [PATCH 10/10] fix --- site/src/content/docs/guides/migration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/content/docs/guides/migration.mdx b/site/src/content/docs/guides/migration.mdx index 19e931836c18..b080af04ccd0 100644 --- a/site/src/content/docs/guides/migration.mdx +++ b/site/src/content/docs/guides/migration.mdx @@ -503,7 +503,7 @@ Bootstrap 6 is a major release with many breaking changes to modernize our codeb - Ratio helpers are now powered by the utility API and use simplified values without `calc()`. - **State variants now use prefix syntax.** Pseudo-state utility classes like hover and focus variants now use a `state:class` prefix pattern (e.g., `hover:opacity-50` instead of `opacity-50-hover`), matching the responsive prefix convention. - **Utility API cleanup.** Removed `css-var`, `css-variable-name`, and `local-vars` options from the utility API. Use the `property` map approach for CSS custom properties and `variables` for static CSS custom properties within utility classes. -- **Shadows are now layered and themeable.** `.shadow`, `.shadow-sm`, and `.shadow-lg` are multi-stop `box-shadow` declarations for a more natural falloff, plus new `.shadow-xs` and `.shadow-xl` sizes. New `.shadow-{color}` utilities (any theme color, plus `.shadow-current`, `.shadow-black`, `.shadow-white`) and `.shadow-opacity-{10-100}` utilities retint and scale shadows. Both set local, non-inheriting CSS variables (`--bs-sc`, `--bs-so`)—mirroring how `.border-{color}` utilities scope `--bs-bc`—so a shadow color or opacity override never leaks into nested `.shadow-*` elements. A global `--bs-shadow-color` and `--bs-shadow-strength` (deepened automatically in dark mode) drive the default, uncolored look. See the [Shadows docs]([[docsref:/utilities/shadows]]) for details. +- **Shadows are now layered and themeable.** `.shadow`, `.shadow-sm`, and `.shadow-lg` are multi-stop `box-shadow` declarations for a more natural falloff, plus new `.shadow-xs` and `.shadow-xl` sizes. New `.shadow-{color}` utilities (any theme color, plus `.shadow-current`, `.shadow-black`, `.shadow-white`) and `.shadow-opacity-{10-100}` utilities re-tint and scale shadows. Both set local, non-inheriting CSS variables (`--bs-sc`, `--bs-so`)—mirroring how `.border-{color}` utilities scope `--bs-bc`—so a shadow color or opacity override never leaks into nested `.shadow-*` elements. A global `--bs-shadow-color` and `--bs-shadow-strength` (deepened automatically in dark mode) drive the default, uncolored look. See the [Shadows docs]([[docsref:/utilities/shadows]]) for details. ### New components and plugins