This content originally appeared on Go Make Things and was authored by Go Make Things
I’ve finally found a kind of CSS-in-JS that doesn’t suck: using modern CSS selectors instead of JavaScript methods to filter out DOM elements.
Let’s dig in!
A use case
Yesterday, I was working on a Kelp web component to close open subnav menus when the user presses the Esc
key or clicks outside of an active subnav/dropdown.
Let’s imagine you have a setup like this example from my subnav article the other day…
<li>
<details open>
<summary>Services</summary>
<ul>
<li><a href="/services">Overview</a></li>
<li><a href="/consulting">Consulting</a></li>
<li><a href="/coaching">Coaching</a></li>
<li><a href="/courses">Courses</a></li>
</ul>
</details>
</li>
Aside: the <details>
and <summary>
elements aren’t the best choice here, but until Popover and Anchor Positioning have better support, it’s what I’m going with.
When a user clicks/taps anywhere, I want to…
- Close all
<details>
elements that are[open]
, but - If an element inside the
<details>
element currently in:focus
, do not close it.
When a user presses the Esc
key, I want to…
- Close all
<details>
elements that are[open]
. - If an element inside a
<details>
element currently in:focus
, shift focus up to the<summary>
element that toggles it.
In summary: don’t close the item if the user clicks inside it. If they Esc
, put focus back on the toggle so they can stay in the normal tab order.
Testing focus using to require JavaScript. But CSS makes it so much easier.
The old way
This used to require using the document.activeElement
property, which in my experience can be a bit finicky.
// Get all open subnav's
const navs = document.querySelectorAll('.navbar details[open]');
const focusedElem = document.activeElement;
// For each subnav
for (const nav of navs) {
// If the element in focus is inside it, skip
const hasFocus = nav.contains(focusedElem);
if (hasFocus) continue;
// Otherwise, close the subnav
nav.removeAttribute('open');
}
It works. I kind of hate it.
The fancy pants new CSS way
With CSS, rather than getting all open subnavs and skipping the ones I don’t want, I can just ones that don’t have an element in focus.
There are two ways to do that:
- The
:focus-within
pseudo-class checks if the element itself or any descendants have focus. - Pairing the
:has()
pseudo-class with:focus()
checks if the element contains an element in focus, excluding the element itself.
For both of these, I can use the :not()
pseudo-class to exclude elements rather than include them.
// Get all open subnav's that aren't currently focused
const navs = document.querySelectorAll('details[open]:not(:focus-within)');
// Close them
for (const nav of navs) {
nav.removeAttribute('open');
}
Much shorter, much simpler.
Another example
In the case of the Esc
key, I always want to close the open <details>
, but I do need to check if there’s an element inside it in focus so I can shift focus to the <summary>
.
Historically, I would do this…
// Get all open subnav's
const navs = document.querySelectorAll('.navbar details[open]');
const focusedElem = document.activeElement;
// Close them
// If focus is inside it, shift focus to toggle
for (const nav of navs) {
const hasFocus = nav.contains(focusedElem);
nav.removeAttribute('open');
if (hasFocus) {
const summary = nav.querySelector('summary');
summary?.focus();
}
}
Now, I can do this…
// Get all open subnav's
const navs = document.querySelectorAll('.navbar details[open]');
// Close them
// If focus is inside it, shift focus to toggle
for (const nav of navs) {
const hasFocus = nav.matches(':has(:focus)');
nav.removeAttribute('open');
if (hasFocus) {
const summary = nav.querySelector('summary');
summary?.focus();
}
}
Not much shorter here, but in my opinion, a lot easier to read, write, and make sense of when I’m looking at the code.
Modern CSS FTW!
Like this? A Lean Web Club membership is the best way to support my work and help me create more free content.
This content originally appeared on Go Make Things and was authored by Go Make Things