I have CSS like this
*:focus-visible {
outline: 2px solid blue; /* This value is dynamically set based on configuration */
}
I now have a button with class toggle
that has
.toggle {
/* ... many other styles */
outline: none;
}
This outline: none
is to prevent the default focus outline from showing when the button is clicked. However, I'd now like the focus-visible
style defined above to apply to it, since focus-visible
styles are only applied in more specific cases, when the focus really should be visible, not just when the button is clicked.
I half expected something like this to work:
.toggle:focus-visible {
outline: inherit;
}
But if I understand this correctly, inherit
does not mean it's inheriting the value from the parent's focus-visible
styles. Is there any way to do this without the .toggle
styles having to duplicate the value set earlier? In other words, is there some way to have it "fall back"?
There is no way (AFAIK) to reset a value to what was set in another, certain ruleset.
Instead setting it to none
by default for .toggle
and then "resetting" it to the "active" value on a certain state, you can clear it only when it is not in that state.
This can be done by setting outline: none
with a selector like .toggle:not(:focus-visible)
.
The code will explain better than I can:
/* As before */
*:focus-visible {
outline: 2px solid blue;
}
/* Do not set it at all here. */
.toggle {
/* outline: none; */
}
/* Remove the outline only when it is not :focus-visible. */
.toggle:not(:focus-visible) {
outline: none;
}
This has a distinct benefit over the other approaches: users with browsers that do not support :focus-visible
will get their browser's default focus ring, instead of nothing — because the last ruleset is dropped due to an unsupported/unrecognized and therefore "invalid" pseudo-class.
Another option would be to use a CSS custom property (AKA CSS variable) to hold the initial, default value. This would of course require that you can configure the the configuration system to set this custom property instead of outline
directly.
Then you could do something like this:
/* Set a CSS var here instead of `outline`. */
*:focus-visible {
--outline: 2px solid blue;
}
/* Use the outline if specified, else none. */
.toggle {
outline: var(--outline, none);
}
This defaults to using none
, but will use the value of the --outline
var if it is defined.
A possible downside to the basic approach is that everything that relied on the outline being set would now have to set it from --outline
. Instead, you can set the outline
property as well: "naive" elements will just use it, but any that need it can be customized (such as buttons). As follows:
*:focus-visible {
--outline: 2px solid blue;
outline: var(--outline); /* This is new */
}
.toggle {
outline: var(--outline, none);
}
This approach does unfortunately force outline
to none
unless --outline
is set, so it disables default outline behavior, such as the browser's focus indicator.
A last-ditch option: use !important
in the *:focus-visible
rule, like so:
*:focus-visible {
outline: 2px solid blue !important;
}
This should be avoided if at all possible, because !important
makes things complicated and harder to maintain and modify in the long run. But sometimes you gotta do what you gotta do (although it usually means there are flaws somewhere in the system).