If you want to be kept up to date with new articles, CSS resources and tools, join our newsletter.
The Track focus debug tool in Polypane shows a floating outline that follows the keyboard focus around the page. This makes it easier to keep track of where the focus is and lets you determine (along with the focus overlay in Polypane) if the focus order makes sense.
Our debug tool uses JavaScript. With CSS Anchor positioning we can get the same effect without any JavaScript at all, so let's see how that works.
The Track focus debug tool is based on the floating focus concept by Guido Bouman at Q42. Be sure to read his article, which also includes accessibility research.
Why CSS-only?
Our JavaScript implementation works well, but measuring and keeping track of elements on the JavaScript main thread is not ideal, since it takes the browser extra work and could interfere with the JS already on the page.
Using only CSS means all the work is offloaded to the browser's rendering engine, which can optimize it better. That means less CPU usage, better battery life and smoother animations.
CSS Anchor positioning
CSS Anchor positioning allows you to position elements relative to other elements on the page. It's supported in Chromium (Polypane included) and Safari as of version 26, provided the elements are keyboard focusable. Support in Firefox is underway.
In Safari, button elements are not keyboard focusable by default. Users must enable this in Safari's settings under Advanced > "Press Tab to highlight each item on a webpage". On your own pages, you can make elements keyboard focusable by adding a
tabindex="0"
attribute to them. We have done so in the demo below, but be aware that users might not expect this behavior if they're used to Safari's default.
This post isn't a tutorial on how to use CSS Anchor positioning, since there are quite a few parts of it that we don't need for our focus tracking. If you want to learn more about Anchor positioning, here are some great resources:
- The Basics of Anchor Positioning by Ahmad Shadeed
- Introducing the CSS anchor positioning API by Una Kravets for the Chrome Developers blog
- One of Those “Onboarding” UIs, With Anchor Positioning by Ryan Trimble
Setting up focus tracking with CSS Anchor positioning
For CSS Anchor positioning, we need two things:
- An element that anchors itself.
- An element that the first element is anchored to.
Linking these two element is done by setting an anchor-name
on the element that can be anchored to, and referencing that in the position-anchor
property on the element that does the anchoring.
If we want to have an indicator that follows the focus, we need to set the anchor-name on whatever element has the focus:
*:focus {
anchor-name: --focus-anchor;
}
That's all the CSS we need for the element that can be anchored to.
Now we need to create the element that will follow the focus. In our JavaScript implementation we use a custom web component for this. For this CSS-only version, we're going to use the ::before
pseudo-element on the html
, so that we don't need to add any extra elements to the page:
html::before {
content: '';
position: fixed;
position-anchor: --focus-anchor;
}
This by itself doesn't do anything, because though we've set and referenced an anchor, we haven't yet told it how to anchor itself. For that, we use the inset properties with the new anchor()
function:
html::before {
content: '';
position: fixed;
position-anchor: --focus-anchor;
inset: anchor(inside);
}
The anchor()
function returns positioning from the element that you're anchored to. The inside
keyword is a shorthand that sets all four inset properties (top
, right
, bottom
, and left
) to the corresponding values from the anchored element.
It's nice if the focus indicator is slightly bigger than the focused element, so it surrounds it without overlapping it.
While we can use the anchor function in a calc by setting top: calc(anchor(top) - 6px)
, right: calc(anchor(right) - 6px)
and so on, it's easier to use a negative margin to achieve the same effect on all sides at once:
html::before {
content: '';
position: fixed;
position-anchor: --focus-anchor;
inset: anchor(inside);
margin: -6px;
}
You still won't see anything, because we haven't given our focus indicator any style. Let's add some styling to make it visible:
html::before {
content: '';
position: fixed;
position-anchor: --focus-anchor;
inset: anchor(inside);
margin: -6px;
display: block;
background: #ff02;
border: 2px solid #3d80a2;
border-radius: 6px;
pointer-events: none;
z-index: 2147483647;
transition: inset 0.2s linear;
}
This gives our focus indicator a semi-transparent background and a border. The pointer-events: none
ensures that it doesn't interfere with clicking or focusing elements on the page. The z-index
is set as high as possible to ensure it's on top of other elements. Lastly we add a transition to the inset
property, which will animate the movement of the pseudo-element when the focus changes.
2147483647
for the z-index seems an arbitrary number, but it's actually the highest possible value for a 32-bit signed integer, which is what browsers use for z-index values.
Hiding the indicator when there's no focus
When an element isn't anchored (in our case, when there are no focused elements), the element is unfortunately still visible because even though it has no width and height, it still has a border.
As of now there is no way to show anchor elements only when they're anchored, so we need to find another workaround.
Jeroen suggested using :focus-within
, since that will tell us if there is any element that's focused. By combining it with :not()
, we can select the ::before
pseudo-element when there are no focused elements and hide it:
html:not(:focus-within)::before {
opacity: 0;
}
Hiding the default focus outline, progressively enhanced
Browsers by default give focused elements an outline, and it's part of your site that you can also implicitly design (by styling :focus-visible
in a way that fits your design). Now that we built our own focus indicator, we want to hide that default outline.
Since CSS Anchor positioning isn't supported everywhere yet, we should make sure that we only replace that default outline with our floating one in browsers that have support. We can do that by only hiding the outline when position-anchor
is supported:
@supports (position-anchor: --test) {
*:focus {
outline: none;
}
}
And while we're at it, we can also wrap all the other CSS in that @supports
rule, so that none of it is applied in browsers that don't support it. Thanks Krijn for suggesting I add the progressive enhancement logic!
There is also a Anchor Positioning Polyfill available that you can include on your site to add support for browsers that don't support it natively yet.
Result and Demo
And that's it! Here's all the CSS together:
@supports (position-anchor: --test) {
*:focus {
anchor-name: --focus-anchor;
outline: none;
}
html::before {
content: '';
position: fixed;
position-anchor: --focus-anchor;
inset: anchor(inside);
margin: -6px;
display: block;
background: #ff02;
border: 2px solid #3d80a2;
border-radius: 6px;
pointer-events: none;
z-index: 2147483647;
opacity: 1;
transition: inset 0.2s linear;
}
html:not(:focus-within)::before {
opacity: 0;
}
}
With just those few lines of CSS, we have a floating focus indicator that follows the keyboard focus around the page.
Try it out below (In Polypane, Chromium or Safari) by clicking one of the buttons and then using the Tab key to move the focus around:
Or try it out on CodePen: