Focus ring
Opt-in keyboard focus indicator that replaces the browser default with a styled outline built from CSS custom properties for width, color, and offset.
.focus-ring applies an outline on :focus-visible, replacing the browser's unstyled default indicator. The ring reads its width, color, and offset from CSS custom properties set at :root, so overriding those properties on any ancestor element scopes the change to that subtree. Because the selector targets :focus-visible, the ring appears during keyboard navigation and stays hidden on pointer focus.
<a href="#" class="d-inline-flex focus-ring py-2xsmall px-xsmall text-decoration-none border rounded">
Custom focus ring
</a>Context variants
.focus-ring derives its color from --focus-ring-color, which reads --cue-color from the cascade. Placing .focus-ring inside a context class automatically shifts the ring to that context's cue color.
<p><a href="#" class="focus-ring context default py-2xsmall px-xsmall text-decoration-none border rounded">Default focus</a></p>
<p><a href="#" class="focus-ring context alternate py-2xsmall px-xsmall text-decoration-none border rounded">Alternate focus</a></p>
<p><a href="#" class="focus-ring context primary py-2xsmall px-xsmall text-decoration-none border rounded">Primary focus</a></p>
<p><a href="#" class="focus-ring context secondary py-2xsmall px-xsmall text-decoration-none border rounded">Secondary focus</a></p>
<p><a href="#" class="focus-ring context neutral py-2xsmall px-xsmall text-decoration-none border rounded">Neutral focus</a></p>
<p><a href="#" class="focus-ring context danger py-2xsmall px-xsmall text-decoration-none border rounded">Danger focus</a></p>
<p><a href="#" class="focus-ring context success py-2xsmall px-xsmall text-decoration-none border rounded">Success focus</a></p>
<p><a href="#" class="focus-ring context warning py-2xsmall px-xsmall text-decoration-none border rounded">Warning focus</a></p>
<p><a href="#" class="focus-ring context info py-2xsmall px-xsmall text-decoration-none border rounded">Info focus</a></p>
<p><a href="#" class="focus-ring context black py-2xsmall px-xsmall text-decoration-none border rounded">Black focus</a></p>
<p><a href="#" class="focus-ring context white py-2xsmall px-xsmall text-decoration-none border rounded">White focus</a></p>Customization
Override --focus-ring-* properties inline or on a parent element to adjust the ring for a specific component without changing the global defaults.
<a href="#" class="d-inline-flex focus-ring py-2xsmall px-xsmall text-decoration-none border rounded" style="
--cx-focus-ring-color: color-mix(in oklch, var(--cx-primary) 50%, var(--cx-secondary));
--cx-focus-ring-width: .25rem;
--cx-focus-ring-offset: .125rem;
">
Custom focus ring
</a>CSS
.focus-ring calls the focus-ring() mixin on :focus-visible, emitting an outline plus outline-offset from the ring custom properties.
.focus-ring:focus-visible {
@include focus-ring(true);
}Custom properties
Five custom properties on :root govern the ring. --focus-ring-color reads --cue-color from the cascade, so the ring adapts automatically inside context classes. --focus-ring-light-opacity and --focus-ring-dark-opacity control the transparency of the cue color in each color scheme.
--focus-ring-width: #{$focus-ring-width};
--focus-ring-light-opacity: #{$focus-ring-light-opacity};
--focus-ring-dark-opacity: #{$focus-ring-dark-opacity};
--focus-ring-color: #{$focus-ring-color};
--focus-ring-offset: #{$focus-ring-offset};
// --focus-ring: #{$focus-ring};Sass variables
Sass variables compile the initial values for the :root custom properties above.
$focus-ring-width: .1875rem;
$focus-ring-light-opacity: .4;
$focus-ring-dark-opacity: .6;
$focus-ring-color: light-dark(to-opacity(var(--cue-color), var(--focus-ring-light-opacity)), to-opacity(var(--cue-color), var(--focus-ring-dark-opacity)));
// $focus-ring: var(--focus-ring-width) solid var(--focus-ring-color);
$focus-ring-offset: 0px;Sass mixin
focus-ring() emits the outline declaration for use in custom selectors. Pass true as the first argument to include outline-offset; pass a color string as the second to hard-code a ring color instead of reading --focus-ring-color.
.custom-selector {
&:focus-visible {
@include focus-ring(true);
}
}@mixin focus-ring($offset: false, $color: null) {
@if $color != null {
outline: var(--focus-ring-width) solid #{$color};
} @else {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
}
@if $offset {
outline-offset: var(--focus-ring-offset);
}
}