Gerillass 3.0.0 is out. One gradient mixin replaces two, and text-gradient takes its colours first, so read the migration guide before you upgrade.
Gerillass
v3.0.0* You can call mixins with or without the gls- namespace (e.g. @include gls-focus-ring();).
The Focus Ring Sass mixin draws the ring a keyboard user follows around the page. It writes an outline on :focus-visible, with a width, an offset and a colour.
That is two declarations, and both halves of the selector matter. The usual ways of doing this by hand either take the ring away from keyboard users or draw one that disappears in high contrast mode.
What goes wrong by hand
Measured in Chrome 152, on buttons, a link and a text input:
| Written by hand | What happens |
|---|---|
:focus { outline: none; } | tabbing to the element shows nothing at all: no outline, no shadow |
a ring on :focus | it also appears on every mouse click, which is why people remove it |
a ring on :focus-visible | a mouse click on a button or a link does not show it; Tab does |
a box-shadow ring instead of an outline | looks the same, but forced-colors mode sets box-shadow to none |
The ring must be an outline. Forced-colors mode, which is how Windows High Contrast reaches CSS, removes box shadows and repaints the outline colour with a system colour. A shadow ring disappears for exactly the people who rely on it most, while an outline stays. This follows the CSS Color Adjustment specification; the browser used for the other measurements on this page could not emulate the mode.
Form fields show the ring on a click too. In Chrome 152 an input, a textarea and a select all match :focus-visible when clicked, not only when reached with Tab, because a browser expects the keyboard to be used next. That is the browser's decision, not the mixin's.
Arguments
| Name | Type | Description |
|---|---|---|
$width (2px) | number (with unit) | The thickness of the ring. Accepts a length, thin, medium, thick, var() or a maths function such as calc(). Not a percentage. |
$offset (2px) | number (with unit) | The gap between the element and the ring. A negative value draws the ring inside the element. Accepts a length, a CSS-wide keyword, var() or a maths function. Not a percentage. |
$color (currentColor) | color | The colour of the ring. The default follows the text colour of the element, which is right for text and outlined controls and wrong for a filled button with light text, where the ring would come out white. Pass a colour there: a colour, currentColor, a system colour such as Highlight, or a function such as var() or color-mix(). |
The values are written straight into outline and outline-offset, so anything CSS accepts there works, including var().
Examples
Buttons. The outlined button keeps the default, because its text is dark. The filled button has light text, so currentColor would draw a white ring, and it is given a colour instead. Click an empty spot in the result, then press the Tab key on your keyboard: a click on a button itself does not show the ring, which is the point of :focus-visible.
.btn-outline {
@include focus-ring;
}
.btn-primary {
@include focus-ring($color: #18181b);
}.btn-outline:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.btn-primary:focus-visible {
outline: 2px solid #18181b;
outline-offset: 2px;
}Form fields. Click into any of them: form fields show the ring on a click as well as with the Tab key. Measured in Chrome 152 on the input, the select and the textarea.
.input,
.textarea,
.select {
@include focus-ring(2px, 2px, #18181b);
}.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible {
outline: 2px solid #18181b;
outline-offset: 2px;
}Inside a container with overflow: hidden, use a negative offset. An outline drawn outside the element is clipped by that container: with a 2px offset only the corners of the ring were left showing. An offset of -4px draws it inside the element, where nothing clips it. The ring also follows border-radius, so a rounded button gets a rounded ring.
A menu inside a card that clips its content. The items fill the card edge to edge, so the ring is drawn 4px inside each of them. Click an empty spot in the result, then press the Tab key on your keyboard.
.menu-item {
@include focus-ring(2px, -4px, #18181b);
}.menu-item:focus-visible {
outline: 2px solid #18181b;
outline-offset: -4px;
}What it refuses
A colour the outline drops stops the build, because a dropped outline means no ring at all, which is the failure this mixin exists to prevent. Measured in Chrome 152, outline: 2px solid drops a word that is not a colour, a quoted colour, a length, and a CSS-wide keyword, which only works as a whole declaration. Until 2.3.1 each of those compiled.
.element {
@include focus-ring(2px, 2px, inherit);
}Error: `inherit` is not a valid $color for `focus-ring`: a CSS-wide keyword only works as a whole declaration, not inside this value. A browser drops the outline, so no ring is drawn. Pass a colour such as `#1a73e8`, `currentColor`, or a function such as `var(--focus)`.A width or offset the browser drops stops the build too. Measured in Chrome 152, the outline drops a percentage, a negative width and a word other than thin, medium or thick, and outline-offset drops a percentage and any word but a CSS-wide keyword. Until 2.3.1 these compiled.
.element {
@include focus-ring(5%);
}Error: `5%` is not a valid $width for `focus-ring`: a percentage does not work here. A browser drops the outline, so no ring is drawn.