Skip to content

Instantly share code, notes, and snippets.

@fpapado
Last active August 2, 2024 16:42
Show Gist options
  • Save fpapado/b9d2487f8c310690ffddaef57405aa86 to your computer and use it in GitHub Desktop.
Save fpapado/b9d2487f8c310690ffddaef57405aa86 to your computer and use it in GitHub Desktop.
:focus-visible progressive enhancement mixin

Find the full post on fotis.xyz

:focus-visible is a standard way of only showing focus styles for keyboard and focus-based modalities. When using it, however, you must take care to not remove :focus altogether, where :focus-visible is not supported.

With CSS, we can achieve progressive enhancement of :focus to :focus-visible:

/* Styles where only focus is supported */
button:focus,
a[href]:focus {
  outline: 2px solid blue;
}

/* Reset styles where focus-visible is supported (we'll add them back in the next block) */
button:focus:not(:focus-visible),
a[href]:focus:not(:focus-visible) {
  outline: none;
}

/* The final, focus-visible styles. You could elect to make this even more obvious, 
   if stakeholders or whomever is pushing against focus styles this week are now off your back.
*/
button:focus-visible,
a[href]:focus-visible {
  outline: 2px solid blue;
}

If :focus-visible is not supported, nothing bad happens; the styling is still there. As more browsers gain support, the :focus-visible statements will be in use.

Read more about :focus-visible on MDN

Sass versions

This mixin uses a feature of Sass for passing arguments to content blocks. This feature is, at the time of writing, only supported in Dart Sass (sass on npm). A version of the mixin that does not have the generic version (which requires that feature) is available in the focus_legacy.scss file.

Usage

These Sass mixins help achieve that declaration, for multiple selectors.

There are three separate mixins, two focusing on common use cases, and a fully custom one:

  • focusVisible: a generic focusVisible, giving you "slots" to decide what to render for :focus, :focus reset, and :focus-visible
  • focusVisibleOutline: a focusVisible, which only resets the outline. If you don't need more custom resets, use this one!
  • focusVisibleBoxShadow: a focusVisible, which only resets the box-shadow. Might be easier if you only set/reset box-shadow.

For anything more custom, I recommend you write out the CSS long-hand, or create another mixin on top. Super-customisable, very generic mixins are a pain to maintain :D

Use focusVisibleOutline:

  @include focusVisibleOutline("button", "a[href]") {
    outline: 2px solid blue;
  }

Use focusVisibleBoxShadow:

  @include focusVisibleBoxShadow("button", "a[href]") {
    box-shadow: 0 0 0 4px blue;
  }

Use focusVisible:

This is the most customisable version of the mixin, where the mixin defers to the caller, with an argument representing the slot of content being rendered. The key thing here is then checking which slot is being rendered. For example, you can reset box-shadow, border, or anything else. This is how focusVisibleOutline and focusVisibleBoxShadow are implemented!

  @include focusVisible using ($slot) {
    @if $slot == focus {
      outline: 2px solid transparent;
      box-shadow: 0 0 0 4px blue;
    }
    @if $slot == focusReset {
      box-shadow: none;
    }
    @if $slot == focusVisible {
      box-shadow: 0 0 0 4px blue;
    }
  }
/*
Mixin to progressively-enhance :focus-visible, keeping :focus where not supported
Add an arbitrary list of selectors into the arguments, and the styling in the block.
You will get CSS that works with :focus and :focus-visible
There are three mixins:
- focusVisible: a generic focusVisible, giving you "slots" to decide what to render for :focus, :focus reset, and :focus-visible
- focusVisibleOutline: a focusVisible, which only resets the outline. If you don't need more custom resets, use this one!
- focusVisibleBoxShadow: a focusVisible, which only resets the box-shadow. Might be easier if you only set/reset box-shadow.
...for anything more custom, I recommend you write out the CSS long-hand, or create another mixin on top.
Super-customisable, very generic mixins are a pain to maintain :D
Use focusVisible:
The key thing here is checking which slot is being rendered.
For example, you can reset box-shadow here, or anything else.
This is how focusVisibleOutline and focusVisibleBoxShadow are implemented!
@include focusVisible using ($slot) {
@if $slot == focus {
outline: 2px solid transparent;
box-shadow: 0 0 0 4px blue;
}
@if $slot == focusReset {
box-shadow: none;
}
@if $slot == focusVisible {
box-shadow: 0 0 0 4px blue;
}
}
Use focusVisibleOutline:
@include focusVisibleOutline("button", "a[href]") {
outline: 2px solid blue;
}
Use focusVisibleBoxShadow:
@include focusVisibleBoxShadow("button", "a[href]") {
box-shadow: 0 0 0 4px blue;
}
Output example (compressed):
button:focus,
a[href]:focus {
outline: 2px solid blue;
}
button:focus:not(:focus-visible),
a[href]:focus:not(:focus-visible) {
outline: none;
}
button:focus-visible,
a[href]:focus-visible {
outline: 2px solid blue;
}
*/
/* Progressively-enhanced :focus-visible, resetting outline */
@mixin focusVisibleOutline($selectors...) {
@include focusVisible($selectors...) using ($slot) {
/* Styles where unsupported */
@if $slot == focus {
@content;
}
/* Reset :focus where :focus-visible supported */
@if $slot == focusReset {
// Reset only outline
outline: none;
}
/* Apply styles for :focus-visible */
@if $slot == focusVisible {
// Same as :focus
@content;
}
}
}
/* Progressively-enhanced :focus-visible, resetting box-shadow */
@mixin focusVisibleBoxShadow($selectors...) {
@include focusVisible($selectors...) using ($slot) {
/* Styles where unsupported */
@if $slot == focus {
@content;
}
/* Reset :focus where :focus-visible supported */
@if $slot == focusReset {
// Reset only outline
box-shadow: none;
}
/* Apply styles for :focus-visible */
@if $slot == focusVisible {
// Same as :focus
@content;
}
}
}
/* Progressively-enhanced :focus-visible, with custom blocks for the caller to pick how resets are applied. */
@mixin focusVisible($selectors...) {
@for $i from 0 to length($selectors) {
/* Styles where unsupported */
#{nth($selectors, $i + 1)}:focus {
@content (focus);
}
/* Reset :focus where :focus-visible supported */
#{nth($selectors, $i + 1)}:focus:not(:focus-visible) {
@content (focusReset);
}
/* Apply styles for :focus-visible */
#{nth($selectors, $i + 1)}:focus-visible {
@content (focusVisible);
}
}
}
/* Specialised versions of the mixins, that work with more Sass implementations (not using the content block arguments feature) */
/* Progressively-enhanced :focus-visible, resetting outline */
@mixin focusVisibleOutline($selectors...) {
@for $i from 0 to length($selectors) {
/* Styles where unsupported */
#{nth($selectors, $i + 1)}:focus {
@content;
}
/* Reset :focus where :focus-visible supported */
#{nth($selectors, $i + 1)}:focus:not(:focus-visible) {
outline: none;
}
/* Apply styles for :focus-visible */
#{nth($selectors, $i + 1)}:focus-visible {
@content;
}
}
}
/* Progressively-enhanced :focus-visible, resetting box-shadow */
@mixin focusVisibleBoxShadow($selectors...) {
@for $i from 0 to length($selectors) {
/* Styles where unsupported */
#{nth($selectors, $i + 1)}:focus {
@content;
}
/* Reset :focus where :focus-visible supported */
#{nth($selectors, $i + 1)}:focus:not(:focus-visible) {
box-shadow: none;
}
/* Apply styles for :focus-visible */
#{nth($selectors, $i + 1)}:focus-visible {
@content;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment