diff --git a/.changeset/focus-ring-mixin.md b/.changeset/focus-ring-mixin.md
new file mode 100644
index 000000000..2addc6389
--- /dev/null
+++ b/.changeset/focus-ring-mixin.md
@@ -0,0 +1,6 @@
+---
+'@tabler/core': minor
+---
+
+Changed the focus indicator from a `box-shadow` ring to an `outline`, following Bootstrap v6. `focus-ring($offset, $color)` is the single focus mixin, driven by the new `--focus-ring`, `--focus-ring-offset` and `--focus-ring-color` tokens. Removed the `$*-focus-box-shadow` Sass variables and the
+`--btn-focus-box-shadow`, `--pagination-focus-box-shadow`, `--accordion-btn-focus-box-shadow` and `--btn-close-focus-shadow` custom properties.
diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss
index 8ee0837a7..55afcc26f 100644
--- a/core/scss/_variables.scss
+++ b/core/scss/_variables.scss
@@ -981,16 +981,13 @@ $box-shadow-inset: inset 0 1px 2px color-transparent(var(--shadow-color), 0.1875
$component-active-color: $white !default;
$component-active-bg: $primary !default;
-// Focus (WCAG 2.2 SC 1.4.11)
-$focus-ring-width: 0.25rem !default;
-$focus-ring-opacity: 0.25 !default;
-$focus-ring-color: color-transparent(var(--primary), $focus-ring-opacity) !default;
-$focus-ring-blur: 0 !default;
-// Inner 2px solid ring keeps the indicator above the 3:1 non-text contrast minimum;
-// the translucent halo preserves the soft look
-$focus-ring-box-shadow:
- 0 0 0 2px var(--primary),
- 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default;
+// Focus
+$focus-ring-width: 2px !default;
+$focus-ring-offset: 2px !default;
+$focus-ring-transition: outline-color 0.15s ease-in-out !default;
+$focus-ring-opacity: 0.5 !default;
+$focus-ring-opacity-dark: 0.75 !default;
+$focus-ring-color: focus-ring-color(var(--primary)) !default;
// Transitions
$transition-base: all 0.2s ease-in-out !default;
@@ -1061,11 +1058,6 @@ $badge-padding-x-lg: 0.5rem !default;
// Buttons + Forms
$input-btn-border-width: var(--border-width) !default;
$input-btn-font-family: var(--body-font-family) !default;
-$input-btn-focus-width: 0.25rem !default;
-$input-btn-focus-color-opacity: $focus-ring-opacity !default;
-$input-btn-focus-color: $focus-ring-color !default;
-$input-btn-focus-blur: $focus-ring-blur !default;
-$input-btn-focus-box-shadow: $focus-ring-box-shadow !default;
$input-btn-padding-y-sm: 0.3125rem !default;
$input-btn-padding-x-sm: 0.5rem !default;
@@ -1096,7 +1088,6 @@ $btn-border-width: $input-btn-border-width !default;
$btn-font-weight: var(--font-weight-medium) !default;
$btn-box-shadow: var(--shadow-input) !default;
-$btn-focus-box-shadow: $focus-ring-box-shadow !default;
$btn-disabled-opacity: 0.4 !default;
$btn-active-box-shadow: inset 0 3px 5px color-transparent(var(--black), 0.125) !default;
@@ -1115,6 +1106,7 @@ $btn-transition:
color 0.15s ease-in-out,
background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out,
+ $focus-ring-transition,
box-shadow 0.15s ease-in-out !default;
$btn-hover-bg-shade-amount: 15% !default;
@@ -1169,10 +1161,9 @@ $input-plaintext-color: var(--body-color) !default;
$input-focus-bg: $input-bg !default;
$input-focus-border-color: mix-color-css(var(--white), var(--primary), 50%) !default;
$input-focus-color: var(--body-color) !default;
-$input-focus-width: $input-btn-focus-width !default;
-$input-focus-box-shadow: $input-btn-focus-box-shadow !default;
$input-transition:
border-color 0.15s ease-in-out,
+ $focus-ring-transition,
box-shadow 0.15s ease-in-out !default;
$input-border-width: $input-btn-border-width !default;
@@ -1303,7 +1294,6 @@ $accordion-button-active-color: inherit !default;
$accordion-btn-gap: 0.75rem !default;
$accordion-header-gap: 1rem !default;
$accordion-tabs-gap: 0.75rem !default;
-$accordion-btn-focus-box-shadow: $focus-ring-box-shadow !default;
// scss-docs-end accordion-variables
// Carousel
@@ -1351,7 +1341,6 @@ $btn-close-padding-x: 0.875em !default;
$btn-close-padding-y: $btn-close-padding-x !default;
$btn-close-color: $body-color !default;
$btn-close-bg: url("data:image/svg+xml,") !default;
-$btn-close-focus-shadow: $focus-ring-box-shadow !default;
$btn-close-opacity: 0.4 !default;
$btn-close-hover-opacity: 0.75 !default;
$btn-close-focus-opacity: 1 !default;
@@ -1520,9 +1509,9 @@ $nav-link-hover-color: var(--link-hover-color) !default;
$nav-link-transition:
color 0.15s ease-in-out,
background-color 0.15s ease-in-out,
- border-color 0.15s ease-in-out !default;
+ border-color 0.15s ease-in-out,
+ $focus-ring-transition !default;
$nav-link-disabled-color: var(--disabled-color) !default;
-$nav-link-focus-box-shadow: $focus-ring-box-shadow !default;
$nav-link-icon-size: $icon-size !default;
$nav-link-icon-color: color-transparent(var(--nav-link-color), 0.5) !default;
$nav-link-hover-icon-color: color-transparent(var(--nav-link-color), 0.8) !default;
@@ -1681,8 +1670,6 @@ $pagination-border-color: transparent !default;
$pagination-focus-color: var(--link-hover-color) !default;
$pagination-focus-bg: var(--secondary-bg) !default;
-$pagination-focus-box-shadow: $focus-ring-box-shadow !default;
-$pagination-focus-outline: 0 !default;
$pagination-hover-color: var(--link-hover-color) !default;
$pagination-hover-bg: var(--active-bg) !default;
@@ -1700,6 +1687,7 @@ $pagination-transition:
color 0.15s ease-in-out,
background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out,
+ $focus-ring-transition,
box-shadow 0.15s ease-in-out !default;
$pagination-border-radius-sm: var(--border-radius-sm) !default;
@@ -1938,7 +1926,7 @@ $form-check-margin-bottom: 0.75rem !default;
$form-check-padding-start: 2rem !default;
$form-check-label-color: null !default;
$form-check-label-cursor: null !default;
-$form-check-transition: null !default;
+$form-check-transition: $focus-ring-transition !default;
$form-check-input-width: 1.25rem !default;
$form-check-min-height: $font-size-base * $line-height-base !default;
@@ -1948,7 +1936,6 @@ $form-check-input-border: var(--border-width) var(--border-style) $input-border-
$form-check-input-border-radius: var(--border-radius) !default;
$form-check-radio-border-radius: 50% !default;
$form-check-input-focus-border: $input-focus-border-color !default;
-$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default;
$form-check-input-box-shadow: $input-box-shadow !default;
$form-check-input-checked-bg-size: 1.25rem !default;
@@ -1994,8 +1981,6 @@ $form-select-border-color: $input-border-color !default;
$form-select-border-radius: $input-border-radius !default;
$form-select-box-shadow: var(--shadow-input) !default;
$form-select-focus-border-color: $input-focus-border-color !default;
-$form-select-focus-width: $input-focus-width !default;
-$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default;
$form-select-padding-y-sm: $input-padding-y-sm !default;
$form-select-padding-x-sm: $input-padding-x-sm !default;
$form-select-font-size-sm: $input-font-size-sm !default;
@@ -2013,7 +1998,8 @@ $form-switch-padding-start: $form-switch-width + 0.5rem !default;
$form-switch-border-radius: $form-switch-width !default;
$form-switch-transition:
background-color 0.15s ease-in-out,
- border-color 0.15s ease-in-out !default;
+ border-color 0.15s ease-in-out,
+ $focus-ring-transition !default;
$form-switch-knob-transition: inset-inline-start 0.15s ease-in-out !default;
$form-switch-focus-color: mix-color-css(var(--white), var(--primary), 50%) !default;
$form-switch-checked-color: var(--white) !default;
@@ -2034,15 +2020,13 @@ $form-range-thumb-bg: var(--primary) !default;
$form-range-thumb-border: 2px var(--border-style) $white !default;
$form-range-thumb-border-radius: 1rem !default;
$form-range-thumb-box-shadow: 0 0.1rem 0.25rem color-transparent(var(--black), 0.1) !default;
-$form-range-thumb-focus-box-shadow:
- 0 0 0 1px $body-bg,
- $input-focus-box-shadow !default;
$form-range-thumb-focus-box-shadow-width: 0.125rem !default;
$form-range-thumb-active-bg: mix-color-css(var(--white), var(--primary), 70%) !default;
$form-range-thumb-disabled-bg: var(--secondary-color) !default;
$form-range-thumb-transition:
background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out,
+ $focus-ring-transition,
box-shadow 0.15s ease-in-out !default;
// Form floating
@@ -2100,7 +2084,7 @@ $form-validation-states: (
'icon': $form-feedback-icon-valid,
'tooltip-color': #fff,
'tooltip-bg-color': var(--success),
- 'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width color-transparent(var(--success), $input-btn-focus-color-opacity),
+ 'focus-ring-color': focus-ring-color(var(--success)),
'border-color': var(--form-valid-border-color),
),
'invalid': (
@@ -2108,7 +2092,7 @@ $form-validation-states: (
'icon': $form-feedback-icon-invalid,
'tooltip-color': #fff,
'tooltip-bg-color': var(--danger),
- 'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width color-transparent(var(--danger), $input-btn-focus-color-opacity),
+ 'focus-ring-color': focus-ring-color(var(--danger)),
'border-color': var(--form-invalid-border-color),
),
) !default;
diff --git a/core/scss/bootstrap/_buttons.scss b/core/scss/bootstrap/_buttons.scss
index 9d92502ff..6304c4d95 100644
--- a/core/scss/bootstrap/_buttons.scss
+++ b/core/scss/bootstrap/_buttons.scss
@@ -29,7 +29,6 @@
// Zero-size shadow, not `none`: the active state is combined with the focus
// ring in one `box-shadow` list, where `none` would negate every other shadow
--btn-active-shadow: 0 0 0 #000;
- --btn-focus-box-shadow: #{$btn-focus-box-shadow};
// scss-docs-end btn-css-vars
display: inline-block;
@@ -50,6 +49,8 @@
@include gradient-bg(var(--btn-bg));
@include box-shadow(var(--btn-box-shadow));
@include transition($btn-transition);
+ // Start the focus ring's `outline-color` transition from transparent, not `currentcolor`.
+ outline-color: transparent;
&:hover {
color: var(--btn-hover-color);
@@ -69,24 +70,12 @@
color: var(--btn-hover-color);
@include gradient-bg(var(--btn-hover-bg));
border-color: var(--btn-hover-border-color);
- outline: 0;
- // Avoid using mixin so we can pass custom focus shadow properly
- @if $enable-shadows {
- box-shadow: var(--btn-box-shadow), var(--btn-focus-box-shadow);
- } @else {
- box-shadow: var(--btn-focus-box-shadow);
- }
+ @include focus-ring(true);
}
.btn-check:focus-visible + & {
border-color: var(--btn-hover-border-color);
- outline: 0;
- // Avoid using mixin so we can pass custom focus shadow properly
- @if $enable-shadows {
- box-shadow: var(--btn-box-shadow), var(--btn-focus-box-shadow);
- } @else {
- box-shadow: var(--btn-focus-box-shadow);
- }
+ @include focus-ring(true);
}
.btn-check:checked + &,
@@ -100,24 +89,6 @@
background-image: if(sass($enable-gradients): none; else: null);
border-color: var(--btn-active-border-color);
@include box-shadow(var(--btn-active-shadow));
-
- &:focus-visible {
- // Avoid using mixin so we can pass custom focus shadow properly
- @if $enable-shadows {
- box-shadow: var(--btn-active-shadow), var(--btn-focus-box-shadow);
- } @else {
- box-shadow: var(--btn-focus-box-shadow);
- }
- }
- }
-
- .btn-check:checked:focus-visible + & {
- // Avoid using mixin so we can pass custom focus shadow properly
- @if $enable-shadows {
- box-shadow: var(--btn-active-shadow), var(--btn-focus-box-shadow);
- } @else {
- box-shadow: var(--btn-focus-box-shadow);
- }
}
&:disabled,
diff --git a/core/scss/bootstrap/_nav.scss b/core/scss/bootstrap/_nav.scss
index 43b576a5d..f905e6cf5 100644
--- a/core/scss/bootstrap/_nav.scss
+++ b/core/scss/bootstrap/_nav.scss
@@ -33,6 +33,7 @@
background: none;
border: 0;
@include transition($nav-link-transition);
+ outline-color: transparent;
&:hover,
&:focus {
@@ -41,8 +42,7 @@
}
&:focus-visible {
- outline: 0;
- box-shadow: $nav-link-focus-box-shadow;
+ @include focus-ring(true);
}
// Disabled state lightens text
diff --git a/core/scss/bootstrap/_pagination.scss b/core/scss/bootstrap/_pagination.scss
index 15a10fdcf..ebfa0e8f5 100644
--- a/core/scss/bootstrap/_pagination.scss
+++ b/core/scss/bootstrap/_pagination.scss
@@ -15,7 +15,6 @@
--pagination-hover-border-color: #{$pagination-hover-border-color};
--pagination-focus-color: #{$pagination-focus-color};
--pagination-focus-bg: #{$pagination-focus-bg};
- --pagination-focus-box-shadow: #{$pagination-focus-box-shadow};
--pagination-active-color: #{$pagination-active-color};
--pagination-active-bg: #{$pagination-active-bg};
--pagination-active-border-color: #{$pagination-active-border-color};
@@ -38,6 +37,8 @@
background-color: var(--pagination-bg);
border: var(--pagination-border-width) solid var(--pagination-border-color);
@include transition($pagination-transition);
+ // Start the focus ring's `outline-color` transition from transparent, not `currentcolor`.
+ outline-color: transparent;
&:hover {
z-index: 2;
@@ -51,8 +52,11 @@
z-index: 3;
color: var(--pagination-focus-color);
background-color: var(--pagination-focus-bg);
- outline: $pagination-focus-outline;
- box-shadow: var(--pagination-focus-box-shadow);
+ outline: 0;
+ }
+
+ &:focus-visible {
+ @include focus-ring(true);
}
&.active,
diff --git a/core/scss/bootstrap/_root.scss b/core/scss/bootstrap/_root.scss
index e49a771cb..2f16d313b 100644
--- a/core/scss/bootstrap/_root.scss
+++ b/core/scss/bootstrap/_root.scss
@@ -124,8 +124,11 @@
// Focus styles
// scss-docs-start root-focus-variables
--focus-ring-width: #{$focus-ring-width};
+ --focus-ring-offset: #{$focus-ring-offset};
--focus-ring-opacity: #{$focus-ring-opacity};
+ --focus-ring-opacity-dark: #{$focus-ring-opacity-dark};
--focus-ring-color: #{$focus-ring-color};
+ --focus-ring: var(--focus-ring-width) solid var(--focus-ring-color);
// scss-docs-end root-focus-variables
// scss-docs-start root-form-validation-variables
diff --git a/core/scss/bootstrap/forms/_form-check.scss b/core/scss/bootstrap/forms/_form-check.scss
index 8ccb898ec..3618aa437 100644
--- a/core/scss/bootstrap/forms/_form-check.scss
+++ b/core/scss/bootstrap/forms/_form-check.scss
@@ -43,6 +43,8 @@
border: $form-check-input-border;
print-color-adjust: exact; // Keep themed appearance for print
@include transition($form-check-transition);
+ // Start the focus ring's `outline-color` transition from transparent, not `currentcolor`.
+ outline-color: transparent;
// The check, dash or dot is a pseudo-element painted through a CSS mask, so
// its colour comes from `--form-check-icon-color` instead of a colour baked
@@ -74,10 +76,9 @@
filter: $form-check-input-active-filter;
}
- &:focus {
+ &:focus-visible {
border-color: $form-check-input-focus-border;
- outline: 0;
- box-shadow: $form-check-input-focus-box-shadow;
+ @include focus-ring(true);
}
&:checked {
diff --git a/core/scss/bootstrap/forms/_form-control.scss b/core/scss/bootstrap/forms/_form-control.scss
index dc6484294..cf92336d8 100644
--- a/core/scss/bootstrap/forms/_form-control.scss
+++ b/core/scss/bootstrap/forms/_form-control.scss
@@ -25,6 +25,8 @@
@include box-shadow($input-box-shadow);
@include transition($input-transition);
+ // Start the focus ring's `outline-color` transition from transparent, not `currentcolor`.
+ outline-color: transparent;
&[type='file'] {
overflow: hidden; // prevent pseudo element button overlap
@@ -39,14 +41,8 @@
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
- outline: 0;
-
- @if $enable-shadows {
- @include box-shadow($input-box-shadow, $input-focus-box-shadow);
- } @else {
- // Avoid using mixin so we can pass custom focus shadow properly
- box-shadow: $input-focus-box-shadow;
- }
+ --focus-ring-offset: -1px;
+ @include focus-ring(true);
}
&::-webkit-date-and-time-value {
diff --git a/core/scss/bootstrap/forms/_form-range.scss b/core/scss/bootstrap/forms/_form-range.scss
index 63f7e0091..86477ede0 100644
--- a/core/scss/bootstrap/forms/_form-range.scss
+++ b/core/scss/bootstrap/forms/_form-range.scss
@@ -19,10 +19,10 @@
// Pseudo-elements must be split across multiple rulesets to have an effect.
// No box-shadow() mixin for focus accessibility.
&::-webkit-slider-thumb {
- box-shadow: $form-range-thumb-focus-box-shadow;
+ @include focus-ring(true);
}
&::-moz-range-thumb {
- box-shadow: $form-range-thumb-focus-box-shadow;
+ @include focus-ring(true);
}
}
@@ -40,6 +40,7 @@
@include border-radius($form-range-thumb-border-radius);
@include box-shadow($form-range-thumb-box-shadow);
@include transition($form-range-thumb-transition);
+ outline-color: transparent;
&:active {
@include gradient-bg($form-range-thumb-active-bg);
@@ -66,6 +67,7 @@
@include border-radius($form-range-thumb-border-radius);
@include box-shadow($form-range-thumb-box-shadow);
@include transition($form-range-thumb-transition);
+ outline-color: transparent;
&:active {
@include gradient-bg($form-range-thumb-active-bg);
diff --git a/core/scss/bootstrap/forms/_form-select.scss b/core/scss/bootstrap/forms/_form-select.scss
index 8e047e937..cf57dbf43 100644
--- a/core/scss/bootstrap/forms/_form-select.scss
+++ b/core/scss/bootstrap/forms/_form-select.scss
@@ -26,16 +26,13 @@
@include border-radius($form-select-border-radius, 0);
@include box-shadow($form-select-box-shadow);
@include transition($form-select-transition);
+ // Start the focus ring's `outline-color` transition from transparent, not `currentcolor`.
+ outline-color: transparent;
&:focus {
border-color: $form-select-focus-border-color;
- outline: 0;
- @if $enable-shadows {
- @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow);
- } @else {
- // Avoid using mixin so we can pass custom focus shadow properly
- box-shadow: $form-select-focus-box-shadow;
- }
+ --focus-ring-offset: -1px;
+ @include focus-ring(true);
}
&[multiple],
diff --git a/core/scss/helpers/_index.scss b/core/scss/helpers/_index.scss
index b0b173c10..9331268ac 100644
--- a/core/scss/helpers/_index.scss
+++ b/core/scss/helpers/_index.scss
@@ -144,8 +144,10 @@
//
// Focus ring
//
-.focus-ring:focus {
- outline: 0;
- // By default, there is no `--bs-focus-ring-x`, `--bs-focus-ring-y`, or `--bs-focus-ring-blur`, but we provide CSS variables with fallbacks to initial `0` values
- box-shadow: var(--focus-ring-x, 0) var(--focus-ring-y, 0) var(--focus-ring-blur, 0) var(--focus-ring-width) var(--focus-ring-color);
+.focus-ring {
+ @include focus-ring-transition();
+}
+
+.focus-ring:focus-visible {
+ @include focus-ring($color: var(--focus-ring-color));
}
diff --git a/core/scss/layout/_page.scss b/core/scss/layout/_page.scss
index 99e069078..fa1dd4b98 100644
--- a/core/scss/layout/_page.scss
+++ b/core/scss/layout/_page.scss
@@ -2,6 +2,7 @@
// Skip-to-content link: hidden via .visually-hidden-focusable until keyboard focus reveals it
.skip-link {
+ @include focus-ring-transition();
position: fixed;
inset-inline-start: 0;
top: 0;
@@ -12,8 +13,7 @@
background: var(--primary);
&:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: 2px;
+ @include focus-ring(true);
}
}
diff --git a/core/scss/mixins/_functions.scss b/core/scss/mixins/_functions.scss
index c2ce2ea00..112d139ad 100644
--- a/core/scss/mixins/_functions.scss
+++ b/core/scss/mixins/_functions.scss
@@ -66,6 +66,13 @@
@return color-mix(in oklab, #{$color} #{to-percentage($alpha)}, $background);
}
+// Focus ring colour of `$color`, as in Bootstrap v6: mixed with the page background
+// instead of made transparent, so the ring is opaque and looks the same on any
+// surface, and stronger in dark mode, where a 50% mix is too faint.
+@function focus-ring-color($color) {
+ @return light-dark(color-transparent($color, calc(var(--focus-ring-opacity) * 100%), var(--body-bg)), color-transparent($color, calc(var(--focus-ring-opacity-dark) * 100%), var(--body-bg)));
+}
+
// Paints a monochrome svg data uri (authored with `black`) in a Sass colour, for
// the few places that cannot use `mask-icon()`: a pseudo-element that already
// carries a background, or a form control without pseudo-elements. Pass a Sass
diff --git a/core/scss/mixins/_mixins.scss b/core/scss/mixins/_mixins.scss
index c1201bdd2..2297b66a4 100644
--- a/core/scss/mixins/_mixins.scss
+++ b/core/scss/mixins/_mixins.scss
@@ -1,3 +1,4 @@
+@use 'sass:list';
@use 'sass:meta';
@use '../settings' as *;
@use '../variables' as *;
@@ -65,15 +66,23 @@
gap: var(--list-gap);
}
-@mixin focus-ring($show-border: false) {
- outline: 0;
- box-shadow:
- 0 0 0 2px var(--primary),
- 0 0 $focus-ring-blur $focus-ring-width color-transparent(var(--primary), 0.25);
-
- @if ($show-border) {
- border-color: color-transparent(var(--primary), 0.25);
+@mixin focus-ring($offset: false, $color: null) {
+ @if $color != null {
+ outline: var(--focus-ring-width) solid #{$color};
+ } @else {
+ outline: var(--focus-ring);
}
+ @if $offset {
+ outline-offset: var(--focus-ring-offset);
+ }
+}
+
+// Fades the focus ring in: `outline-color` starts from transparent instead of
+// `currentcolor`. `transition` is one property, so pass the element's other
+// transitions here instead of a second `transition()` call.
+@mixin focus-ring-transition($transitions...) {
+ outline-color: transparent;
+ @include transition(list.append($transitions, $focus-ring-transition, comma)...);
}
//
diff --git a/core/scss/mixins/bootstrap/_forms.scss b/core/scss/mixins/bootstrap/_forms.scss
index 8f00c082a..bf78e9ca1 100644
--- a/core/scss/mixins/bootstrap/_forms.scss
+++ b/core/scss/mixins/bootstrap/_forms.scss
@@ -4,6 +4,7 @@
@use '../../settings' as *;
@use '../../variables' as *;
@use '../functions' as *;
+@use '../mixins' as *;
@use 'breakpoints' as *;
// This mixin uses an `if()` technique to be compatible with Dart Sass
// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details
@@ -21,7 +22,7 @@
}
}
-@mixin form-validation-state($state, $color, $icon, $tooltip-color: color-contrast($color), $tooltip-bg-color: color-transparent($color, $form-feedback-tooltip-opacity), $focus-box-shadow: 0 0 $input-btn-focus-blur $input-focus-width color-transparent($color, $input-btn-focus-color-opacity), $border-color: $color) {
+@mixin form-validation-state($state, $color, $icon, $tooltip-color: color-contrast($color), $tooltip-bg-color: color-transparent($color, $form-feedback-tooltip-opacity), $focus-ring-color: focus-ring-color($color), $border-color: $color) {
.#{$state}-feedback {
display: none;
width: 100%;
@@ -67,11 +68,7 @@
&:focus {
border-color: $border-color;
- @if $enable-shadows {
- @include box-shadow($input-box-shadow, $focus-box-shadow);
- } @else {
- box-shadow: $focus-box-shadow;
- }
+ @include focus-ring(true, $focus-ring-color);
}
}
}
@@ -102,11 +99,7 @@
&:focus {
border-color: $border-color;
- @if $enable-shadows {
- @include box-shadow($form-select-box-shadow, $focus-box-shadow);
- } @else {
- box-shadow: $focus-box-shadow;
- }
+ @include focus-ring(true, $focus-ring-color);
}
}
}
@@ -128,7 +121,7 @@
}
&:focus {
- box-shadow: $focus-box-shadow;
+ @include focus-ring(true, $focus-ring-color);
}
~ .form-check-label {
diff --git a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
index 69140ea0e..252823c3b 100644
--- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
+++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
@@ -43,6 +43,10 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-chart-4",
"--tblr-chart-5",
"--tblr-danger",
+ "--tblr-focus-ring",
+ "--tblr-focus-ring-offset",
+ "--tblr-focus-ring-opacity",
+ "--tblr-focus-ring-opacity-dark",
"--tblr-font-sans-serif",
"--tblr-font-size-h3",
"--tblr-font-size-h5",
@@ -69,6 +73,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-success",
"--tblr-tertiary",
"--tblr-text-inverted",
+ "--tblr-white",
"--tblr-yellow",
"--ts-pr-caret",
"--ts-pr-clear-button",
diff --git a/core/scss/tests/_colors-misc.test.scss b/core/scss/tests/_colors-misc.test.scss
index 023035075..04794cb8b 100644
--- a/core/scss/tests/_colors-misc.test.scss
+++ b/core/scss/tests/_colors-misc.test.scss
@@ -67,6 +67,12 @@
}
}
+@include describe('focus-ring-color') {
+ @include it('mixes with the page background, stronger in dark mode') {
+ @include assert-equal(meta.inspect(focus-ring-color(var(--primary))), 'light-dark(color-mix(in oklab, var(--primary) calc(var(--focus-ring-opacity) * 100%), var(--body-bg)), color-mix(in oklab, var(--primary) calc(var(--focus-ring-opacity-dark) * 100%), var(--body-bg)))');
+ }
+}
+
@include describe('map-loop') {
// Applies a module-level function (resolved by name via meta.get-function) to
// every value in the map. `to-percentage` lives in the same module, so it
diff --git a/core/scss/tests/_mixins.test.scss b/core/scss/tests/_mixins.test.scss
index faa3774cd..8805ada05 100644
--- a/core/scss/tests/_mixins.test.scss
+++ b/core/scss/tests/_mixins.test.scss
@@ -104,7 +104,7 @@
}
@include describe('focus-ring mixin') {
- @include it('outputs the ring without a border by default') {
+ @include it('outputs the `--focus-ring` outline by default') {
@include assert() {
@include output() {
.t {
@@ -113,29 +113,38 @@
}
@include expect() {
.t {
- outline: 0;
- box-shadow:
- 0 0 0 2px var(--primary),
- 0 0 0 0.25rem color-mix(in oklab, var(--primary) 25%, transparent);
+ outline: var(--focus-ring);
}
}
}
}
- @include it('adds a border colour when requested') {
+ @include it('adds the offset when requested') {
@include assert() {
@include output() {
.t {
- @include focus-ring($show-border: true);
+ @include focus-ring(true);
}
}
@include expect() {
.t {
- outline: 0;
- box-shadow:
- 0 0 0 2px var(--primary),
- 0 0 0 0.25rem color-mix(in oklab, var(--primary) 25%, transparent);
- border-color: color-mix(in oklab, var(--primary) 25%, transparent);
+ outline: var(--focus-ring);
+ outline-offset: var(--focus-ring-offset);
+ }
+ }
+ }
+ }
+
+ @include it('takes a custom colour') {
+ @include assert() {
+ @include output() {
+ .t {
+ @include focus-ring($color: red);
+ }
+ }
+ @include expect() {
+ .t {
+ outline: var(--focus-ring-width) solid red;
}
}
}
diff --git a/core/scss/ui/_accordion.scss b/core/scss/ui/_accordion.scss
index 52907759e..20fef12e6 100644
--- a/core/scss/ui/_accordion.scss
+++ b/core/scss/ui/_accordion.scss
@@ -19,7 +19,6 @@
--accordion-body-padding-y: #{$accordion-body-padding-y};
--accordion-btn-gap: #{$accordion-btn-gap};
--accordion-header-gap: #{$accordion-header-gap};
- --accordion-btn-focus-box-shadow: #{$accordion-btn-focus-box-shadow};
display: flex;
flex-direction: column;
@@ -40,7 +39,7 @@
text-align: inherit;
background-color: var(--accordion-btn-bg);
border: 0;
- transition: transform $transition-time;
+ @include focus-ring-transition(transform $transition-time);
overflow-anchor: none;
&:not(.collapsed) {
@@ -53,15 +52,10 @@
z-index: 2;
}
- &:focus {
+ &:focus-visible {
z-index: 3;
- outline: 0;
- box-shadow: var(--accordion-btn-focus-box-shadow);
-
- &:not(:focus-visible) {
- outline: none;
- box-shadow: none;
- }
+ @include focus-ring(true);
+ outline-offset: -1px;
}
}
diff --git a/core/scss/ui/_buttons.scss b/core/scss/ui/_buttons.scss
index 8aa421cc3..0dd72794c 100644
--- a/core/scss/ui/_buttons.scss
+++ b/core/scss/ui/_buttons.scss
@@ -199,6 +199,7 @@
//
.btn-icon,
.btn-action {
+ @include focus-ring-transition();
//[BUG] btn-sm and btn-xl with an icon looks broken
//issue #2470 fixed
min-width: calc(var(--btn-line-height) + (var(--btn-padding-y) * 2) + (var(--btn-border-width) * 2));
@@ -290,8 +291,8 @@
}
&:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: -2px;
+ @include focus-ring();
+ outline-offset: calc(var(--focus-ring-width) * -1);
box-shadow: none;
}
diff --git a/core/scss/ui/_calendars.scss b/core/scss/ui/_calendars.scss
index c67c53d38..bfbe8b841 100644
--- a/core/scss/ui/_calendars.scss
+++ b/core/scss/ui/_calendars.scss
@@ -60,7 +60,7 @@
border: var(--border-width) var(--border-style) transparent;
@include border-radius(var(--border-radius-pill));
outline: 0;
- @include transition(background $transition-time, border $transition-time, box-shadow 0.32s, color $transition-time);
+ @include focus-ring-transition(background $transition-time, border $transition-time, box-shadow 0.32s, color $transition-time);
&:hover {
color: var(--primary);
@@ -70,8 +70,7 @@
}
&:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: 2px;
+ @include focus-ring(true);
}
}
diff --git a/core/scss/ui/_close.scss b/core/scss/ui/_close.scss
index 79d893780..04f8c5cc6 100644
--- a/core/scss/ui/_close.scss
+++ b/core/scss/ui/_close.scss
@@ -1,11 +1,11 @@
@use '../config' as *;
.btn-close {
+ @include focus-ring-transition();
--btn-close-color: currentColor;
--btn-close-bg: #{escape-svg($btn-close-bg)};
--btn-close-opacity: #{$btn-close-opacity};
--btn-close-hover-opacity: #{$btn-close-hover-opacity};
- --btn-close-focus-shadow: #{$btn-close-focus-shadow};
--btn-close-focus-opacity: #{$btn-close-focus-opacity};
--btn-close-disabled-opacity: #{$btn-close-disabled-opacity};
--btn-close-size: #{$btn-close-width};
@@ -43,10 +43,13 @@
&:focus {
outline: 0;
- box-shadow: var(--btn-close-focus-shadow);
opacity: var(--btn-close-focus-opacity);
}
+ &:focus-visible {
+ @include focus-ring(true);
+ }
+
&:disabled,
&.disabled {
pointer-events: none;
diff --git a/core/scss/ui/_datepicker.scss b/core/scss/ui/_datepicker.scss
index 066a405a6..14fc60b96 100644
--- a/core/scss/ui/_datepicker.scss
+++ b/core/scss/ui/_datepicker.scss
@@ -49,12 +49,13 @@
button {
font-family: inherit;
+ @include focus-ring-transition();
}
button:focus-visible {
position: relative;
z-index: 1;
- @include focus-ring();
+ @include focus-ring(true);
}
}
@@ -312,7 +313,7 @@ button[data-vc-week-day] {
background-color: color-transparent(var(--calendar-day-bg), var(--calendar-day-bg-opacity));
border: 0;
border-radius: var(--calendar-border-radius);
- @include transition(color $transition-time, background-color $transition-time);
+ @include focus-ring-transition(color $transition-time, background-color $transition-time);
&:hover {
--calendar-day-bg: var(--calendar-day-hover-bg);
diff --git a/core/scss/ui/_forms.scss b/core/scss/ui/_forms.scss
index b0849c2e6..e2e8b2a7e 100644
--- a/core/scss/ui/_forms.scss
+++ b/core/scss/ui/_forms.scss
@@ -165,8 +165,10 @@ Input group
}
.input-group-flat {
+ @include focus-ring-transition();
&:focus-within {
- box-shadow: $input-focus-box-shadow;
+ --focus-ring-offset: -1px;
+ @include focus-ring(true);
@include border-radius($input-border-radius);
.form-control,
@@ -178,6 +180,7 @@ Input group
.form-control {
&:focus {
border-color: $input-border-color;
+ outline: 0;
box-shadow: none;
}
diff --git a/core/scss/ui/_legend.scss b/core/scss/ui/_legend.scss
index db2407020..cb58e2907 100644
--- a/core/scss/ui/_legend.scss
+++ b/core/scss/ui/_legend.scss
@@ -42,9 +42,7 @@ Legend
white-space: nowrap;
background: transparent;
border: 0;
- transition:
- background-color $transition-time,
- opacity $transition-time;
+ @include focus-ring-transition(background-color $transition-time, opacity $transition-time);
@include border-radius(var(--border-radius-pill));
.legend-dot {
@@ -56,8 +54,8 @@ Legend
}
&:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: -2px;
+ @include focus-ring();
+ outline-offset: calc(var(--focus-ring-width) * -1);
}
}
diff --git a/core/scss/ui/_nav.scss b/core/scss/ui/_nav.scss
index 9c2f6e13f..7464b5273 100644
--- a/core/scss/ui/_nav.scss
+++ b/core/scss/ui/_nav.scss
@@ -77,7 +77,7 @@
.nav-link {
display: flex;
align-items: center;
- @include transition(color $transition-time, background-color $transition-time);
+ @include transition(color $transition-time, background-color $transition-time, $focus-ring-transition);
&:hover,
&:focus {
diff --git a/core/scss/ui/_switch-icon.scss b/core/scss/ui/_switch-icon.scss
index d75c70582..f211290e5 100644
--- a/core/scss/ui/_switch-icon.scss
+++ b/core/scss/ui/_switch-icon.scss
@@ -1,6 +1,7 @@
@use '../config' as *;
.switch-icon {
+ @include focus-ring-transition();
--switch-icon-size: #{$switch-icon-size};
--switch-icon-min-size: #{$switch-icon-min-size};
--switch-icon-disabled-opacity: #{$switch-icon-disabled-opacity};
@@ -36,8 +37,7 @@
}
&:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: 2px;
+ @include focus-ring(true);
}
svg {
diff --git a/core/scss/ui/forms/_form-check.scss b/core/scss/ui/forms/_form-check.scss
index eb1b81ae0..babd6b69e 100644
--- a/core/scss/ui/forms/_form-check.scss
+++ b/core/scss/ui/forms/_form-check.scss
@@ -69,7 +69,7 @@ Form switch
.form-check-input {
height: $form-switch-height;
margin-top: ($form-check-min-height - $form-switch-height) * 0.5;
- @include transition(background-color $transition-time, background-position $transition-time);
+ @include transition(background-color $transition-time, background-position $transition-time, $focus-ring-transition);
// An off switch that is disabled fills its track like a disabled input,
// so it no longer looks like an off switch that can still be toggled.
diff --git a/core/scss/ui/forms/_form-colorinput.scss b/core/scss/ui/forms/_form-colorinput.scss
index 14c1420b8..18b14f2f4 100644
--- a/core/scss/ui/forms/_form-colorinput.scss
+++ b/core/scss/ui/forms/_form-colorinput.scss
@@ -19,6 +19,7 @@ Color Input
}
.form-colorinput-color {
+ @include focus-ring-transition();
display: block;
width: var(--form-colorinput-size);
height: var(--form-colorinput-size);
@@ -46,7 +47,7 @@ Color Input
.form-colorinput-input:focus ~ & {
border-color: var(--primary);
- box-shadow: $input-btn-focus-box-shadow;
+ @include focus-ring(true);
}
@at-root .form-colorinput-light & {
diff --git a/core/scss/ui/forms/_form-imagecheck.scss b/core/scss/ui/forms/_form-imagecheck.scss
index b617fe641..356b47743 100644
--- a/core/scss/ui/forms/_form-imagecheck.scss
+++ b/core/scss/ui/forms/_form-imagecheck.scss
@@ -19,6 +19,7 @@ Image check
}
.form-imagecheck-figure {
+ @include focus-ring-transition();
position: relative;
display: block;
margin: 0;
@@ -28,7 +29,7 @@ Image check
.form-imagecheck-input:focus ~ & {
border-color: var(--primary);
- box-shadow: $input-btn-focus-box-shadow;
+ @include focus-ring(true);
}
.form-imagecheck-input:checked ~ & {
diff --git a/core/scss/ui/forms/_form-otp.scss b/core/scss/ui/forms/_form-otp.scss
index a66af6f05..293ed50b7 100644
--- a/core/scss/ui/forms/_form-otp.scss
+++ b/core/scss/ui/forms/_form-otp.scss
@@ -88,9 +88,7 @@
border: var(--otp-slot-border-width) solid var(--otp-slot-border-color);
box-shadow: var(--otp-slot-box-shadow);
@include border-radius(var(--otp-slot-border-radius));
- transition:
- border-color 0.15s ease-in-out,
- box-shadow 0.15s ease-in-out;
+ @include focus-ring-transition(border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out);
}
.otp-slot-active {
@@ -98,7 +96,9 @@
// next slot in `.otp-connected`, where slots overlap by their border width.
z-index: 1;
- @include focus-ring($show-border: true);
+ --focus-ring-offset: -1px;
+ border-color: $input-focus-border-color;
+ @include focus-ring(true);
&:not(.otp-slot-filled)::after {
display: block;
diff --git a/core/scss/ui/forms/_form-selectgroup.scss b/core/scss/ui/forms/_form-selectgroup.scss
index 85db7aef2..7527e76d9 100644
--- a/core/scss/ui/forms/_form-selectgroup.scss
+++ b/core/scss/ui/forms/_form-selectgroup.scss
@@ -49,7 +49,7 @@ Select group
border: var(--border-width) var(--border-style) $input-border-color;
box-shadow: $input-box-shadow;
@include border-radius(var(--border-radius));
- @include transition(border-color $transition-time, background $transition-time, color $transition-time);
+ @include focus-ring-transition(border-color $transition-time, background $transition-time, color $transition-time);
> .icon:only-child {
margin: 0 var(--form-selectgroup-icon-margin-x);
@@ -110,7 +110,7 @@ Select group
z-index: 2;
color: var(--primary);
border-color: var(--primary);
- box-shadow: $input-btn-focus-box-shadow;
+ @include focus-ring(true);
}
/**
diff --git a/core/scss/vendor/_coloris.scss b/core/scss/vendor/_coloris.scss
index 396dbb9c3..b6a82da9a 100644
--- a/core/scss/vendor/_coloris.scss
+++ b/core/scss/vendor/_coloris.scss
@@ -11,12 +11,15 @@
}
input.clr-color {
+ @include focus-ring-transition();
@include border-radius(var(--border-radius));
color: var(--body-color);
background: transparent;
border-color: var(--border-color);
&:focus {
+ --focus-ring-offset: -1px;
+ border-color: $input-focus-border-color;
@include focus-ring(true);
}
}
@@ -25,9 +28,10 @@ input.clr-color {
button {
@include border-radius(var(--border-radius));
padding: 0 2px 4px;
+ @include focus-ring-transition();
&:focus {
- @include focus-ring();
+ @include focus-ring(true);
}
}
}
@@ -43,8 +47,10 @@ input.clr-color {
}
button {
+ @include focus-ring-transition();
+
&:focus {
- @include focus-ring();
+ @include focus-ring(true);
}
}
}
@@ -58,13 +64,14 @@ input.clr-color {
width: 1.5rem;
height: 1.5rem;
@include border-radius(var(--border-radius));
+ @include focus-ring-transition();
&::after {
box-shadow: inset 0 0 0 1px var(--border-color-translucent);
}
&:focus {
- @include focus-ring();
+ @include focus-ring(true);
}
}
diff --git a/core/scss/vendor/_nouislider.scss b/core/scss/vendor/_nouislider.scss
index cc2531598..d8ca4127d 100644
--- a/core/scss/vendor/_nouislider.scss
+++ b/core/scss/vendor/_nouislider.scss
@@ -19,6 +19,7 @@
}
.noUi-handle {
+ @include focus-ring-transition();
width: $form-range-thumb-width;
height: $form-range-thumb-height;
background: currentColor;
@@ -43,7 +44,7 @@
&.noUi-active,
&:focus {
- box-shadow: $form-range-thumb-focus-box-shadow;
+ @include focus-ring(true);
}
}
diff --git a/core/scss/vendor/_tom-select.scss b/core/scss/vendor/_tom-select.scss
index 2171bdb6d..2a1e25cc8 100644
--- a/core/scss/vendor/_tom-select.scss
+++ b/core/scss/vendor/_tom-select.scss
@@ -9,7 +9,11 @@ $input-border-width: 1px;
}
.focus .ts-control {
+ @include focus-ring-transition(border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out);
+ --focus-ring-offset: -1px;
+ box-shadow: $input-box-shadow;
@include border-radius(var(--border-radius));
+ @include focus-ring(true);
}
.ts-control {
@@ -62,7 +66,7 @@ $input-border-width: 1px;
&.focus .ts-control {
border-color: map.get($data, 'border-color') !important;
- box-shadow: map.get($data, 'focus-box-shadow') !important;
+ outline-color: map.get($data, 'focus-ring-color');
}
}
}
diff --git a/docs/content/ui/getting-started/upgrade/1-6.mdx b/docs/content/ui/getting-started/upgrade/1-6.mdx
index e9c760428..aa68bec37 100644
--- a/docs/content/ui/getting-started/upgrade/1-6.mdx
+++ b/docs/content/ui/getting-started/upgrade/1-6.mdx
@@ -27,6 +27,7 @@ Hide `path:first-child` in `.accordion-button-toggle-plus` from your own CSS|Dro
Call `tabler.tabler.getColor()` or `tabler.tabler.hexToRgba()`|Read the variable yourself, see [Removed JavaScript helpers](#removed-javascript-helpers)
Use `.form-hint`|Rename to `.form-text` (the old name still works)
Override `$form-switch-bg-image` or the toggler icon variables|See [Masked icons](#masked-icons)
+Override `$input-btn-focus-*`, a `$*-focus-box-shadow` variable or `--tblr-btn-focus-box-shadow`|Use the focus ring tokens, see [The focus ring is an outline](#the-focus-ring-is-an-outline)
Override `$theme-colors-rgb` or use `theme-color-lighter()`|See [Sass changes](#sass-changes)
Rely on the empty gutter next to the scrollbar|See [Visual changes](#visual-changes)
Set `data-mask` or `data-countup` from code after page load|Create the component instead, see [Plugins are components now](#plugins-are-components-now)
@@ -230,6 +231,44 @@ For most projects this is invisible. It matters if you customized those icons in
If you replaced one of the SVGs with your own, keep the shape but drop the `fill` and `stroke` colors from it. The mask only looks at the alpha channel, so any color inside the SVG is ignored.
+## The focus ring is an outline
+
+In 1.5 the focus indicator was a `box-shadow`, a solid 2px ring with a soft `0.25rem` halo around it. In 1.6 it's an `outline`, the way Bootstrap 6 draws it. Every component uses the same three variables on `:root`:
+
+```css
+:root {
+ --tblr-focus-ring-width: 2px;
+ --tblr-focus-ring-offset: 2px;
+ --tblr-focus-ring-color: /* primary mixed with the page background */;
+}
+```
+
+The color is `--tblr-primary` mixed with `--tblr-body-bg`, at `--tblr-focus-ring-opacity` (`0.5`) in light mode and `--tblr-focus-ring-opacity-dark` (`0.75`) in dark mode. It follows `--tblr-primary`, so a recolored theme gets a matching ring for free.
+
+The per-component shadow variables are gone. If you changed the ring of one component, set the ring variables on it instead:
+
+```diff
+ .btn {
+- --tblr-btn-focus-box-shadow: 0 0 0 3px #f11d46;
++ --tblr-focus-ring-width: 3px;
++ --tblr-focus-ring-color: #f11d46;
+ }
+```
+
+The same goes for `--tblr-pagination-focus-box-shadow`, `--tblr-accordion-btn-focus-box-shadow` and `--tblr-btn-close-focus-shadow`.
+
+Form controls draw the ring over their border with `--tblr-focus-ring-offset: -1px`. Buttons and everything else draw it `2px` outside.
+
+If you write your own components, the `focus-ring()` mixin gives them the same ring. Pass `true` to apply the offset, and a color as the second argument to override it:
+
+```scss
+.my-control:focus-visible {
+ @include focus-ring(true);
+}
+```
+
+The `.focus-ring` helper is fixed along the way. It now shows on `:focus-visible` instead of `:focus`, so a mouse click no longer draws it.
+
## Sass changes
Only relevant if you compile `scss/tabler.scss` yourself.
@@ -243,9 +282,25 @@ Variable|What to do instead
`$form-switch-bg-image`, `$form-switch-bg-image-dark`, `$form-switch-checked-bg-image`, `$form-switch-checked-bg-position`, `$form-switch-focus-bg-image`|See [Masked icons](#masked-icons).
`$form-check-input-checked-bg-image-dark`|See [Masked icons](#masked-icons).
`$navbar-light-toggler-icon-bg`, `$navbar-dark-toggler-icon-bg`|Use `$navbar-toggler-icon-bg`.
+`$focus-ring-blur`, `$focus-ring-box-shadow`|Use `$focus-ring-width`, `$focus-ring-offset` and `$focus-ring-color`. See [The focus ring is an outline](#the-focus-ring-is-an-outline).
+`$input-btn-focus-width`, `$input-btn-focus-color-opacity`, `$input-btn-focus-color`, `$input-btn-focus-blur`, `$input-btn-focus-box-shadow`|Same as above.
+`$input-focus-width`, `$input-focus-box-shadow`, `$btn-focus-box-shadow`, `$form-select-focus-width`, `$form-select-focus-box-shadow`, `$form-check-input-focus-box-shadow`, `$form-range-thumb-focus-box-shadow`|Same as above.
+`$nav-link-focus-box-shadow`, `$pagination-focus-box-shadow`, `$pagination-focus-outline`, `$accordion-btn-focus-box-shadow`, `$btn-close-focus-shadow`|Same as above.
Setting a removed variable in `@use … with (…)` is a compile error in Sass, so you'll notice right away.
+In `$form-validation-states` the `focus-box-shadow` key is now `focus-ring-color`, and it takes a color instead of a shadow:
+
+```diff
+ 'invalid': (
+ 'color': var(--form-invalid-color),
+- 'focus-box-shadow': 0 0 0 0.25rem rgba(214, 57, 57, 0.25),
++ 'focus-ring-color': focus-ring-color(var(--danger)),
+ ),
+```
+
+Two defaults changed too: `$focus-ring-width` went from `0.25rem` to `2px`, and `$focus-ring-opacity` from `0.25` to `0.5`.
+
### Removed functions
`theme-color-lighter()` and `theme-color-darker()` computed a tint and a shade at compile time. They were removed with the RGB variables. Two pairs of functions replace them:
@@ -270,6 +325,7 @@ You can still override them with anything Sass accepts. Just be aware that a Sas
None of these break markup, but they change how a page looks or scrolls. Worth a quick look at your screens after the upgrade.
+- **The focus ring is a solid outline.** The soft halo around focused buttons and inputs is gone. Focus is a single 2px ring, stronger in dark mode. See [The focus ring is an outline](#the-focus-ring-is-an-outline).
- **No reserved scrollbar gutter.** 1.5 set `scrollbar-gutter: stable` on `html`, which left an empty strip on the right of the navbar on pages too short to scroll. That rule is gone. Layout shifts between short and long pages are back to the browser default, which on macOS and most mobile browsers means none.
- **Scroll chaining is contained.** Scrolling to the end of a modal body, an offcanvas body or a scrollable dropdown no longer scrolls the page behind it.
- **Native `