CSS is listening to us. No, not like that. Slightly, CSS is accumulating increasingly pseudo-classes to assist us reply to JavaScript occasions in order that we don’t have to take action with JavaScript itself. However whereas pseudo-classes observe states, not occasions, they certain can really feel like occasion listeners typically (not that it actually issues within the context of CSS).
Then once more, what is CSS lately? For instance, there’s a proposal for event-trigger within the Animation Triggers spec, which might principally hear for occasions and set off animations. In the event you ask me although, the syntax is able to much more than that (suppose: invoker instructions however for CSS).
However to remain in in the present day’s actuality, I’ll stroll you thru the completely different CSS pseudo-classes on the market which might be type of like occasion listeners, earlier than doing the identical for event-trigger, the place I’ll present you the way (I feel) this presently unsupported characteristic would work.
“Occasion listening” pseudo-classes
:hover and :lively
The :hover state captures the second from when the pointerenter occasion fires to when the pointerleave occasion fires, which completely illustrates why pseudo-classes are states, not occasions.
:lively matches the goal (e.g., a hyperlink or button) that’s presently being pressed with a mouse, finger, or stylus, which makes it akin to pointerdown and pointerup/pointercancel.
By the way in which, the pointer-events: none CSS declaration prevents pointer occasions from firing on the chosen aspect!
:focus and :focus-visible
The :focus pseudo-class is akin to the focus and blur (unfocus) JavaScript occasions, however :focus-visible is a little more advanced. :focus-visible triggers when :focus does, however as well as, the browser makes use of a wide range of heuristics to find out whether or not or not a spotlight indicator needs to be proven. Is the person working with a keyboard? Is the aspect a kind management? This actually makes me respect what CSS gives. In truth, one of the best ways to deal with this utilizing JavaScript is to question the CSS pseudo-class:
aspect.addEventListener("focus", (occasion) => {
if (occasion.goal.matches(":focus-visible")) {
/* Do one thing */
}
});
:focus-within (and :has())
JavaScript excels on the “if A is Y, then do Z to B” type of stuff. We will traverse the DOM, leverage occasion propagation, and way more. In that regard, CSS can really feel a bit restricted. Nonetheless, CSS is evolving shortly. It has many new if-this-do-that options similar to scroll-driven animations, and it’ll have extra sooner or later. HTML is doing the identical with devoted elements similar to
I’ll point out a few of that later, truly. In a extra holistic sense, what we have now is :focus-within, which matches if a toddler has focus, and :has(), which accepts any legitimate selector and matches if such a relationship exists between the 2 selectors.
For instance, these two selectors do the very same factor:
kind:focus-within {
/* Fashion the shape when one thing inside has focus */
}
kind:has(:focus) {
/* Fashion the shape when one thing inside has focus */
}
:checked
It’s pretty apparent what :checked does. The JavaScript occasion that’s most synonymous with it’s change, which fires when the worth of an , , or adjustments (though, on this context, the enter occasion is kind of comparable).
To hear for a examine, we’d do one thing like this:
checkbox.addEventListener("change", (occasion) => {
if (occasion.goal.checked) {
/* Checked */
} else {
/* Not checked */
}
});
CSS pseudo-classes typically seize the second between two JavaScript occasions (e.g., pointerenter and pointerleave), however once they’re not doing that, they’re dealing with logic as an alternative, as above.
Let’s take a look at some extra examples of hidden logic dealing with.
:legitimate/:invalid/:user-valid/:user-invalid/:autofill
We don’t want the :not() pseudo-class perform right here, as validity might be checked utilizing each the :legitimate and :invalid pseudo-classes, however on the JavaScript facet of issues, there’s no legitimate occasion (solely invalid). That being stated, if utilizing JavaScript, you’ll probably wish to name the checkValidity() methodology (which truly fires the invalid occasion if it returns false) inside the callback of the occasion listener for enter, change, blur (to examine validity when unfocusing from a component), or submit (to examine validity of the whole kind when submitting it, as beneath).
kind.addEventListener("submit", () => {
if (kind.checkValidity()) {
/* All kind controls are legitimate */
} else {
/* A kind management is invalid (the invalid occasion fires) */
}
});
We will additionally do that with the ValidityState object, which doesn’t hearth the invalid occasion, however does inform us why a kind management is legitimate or invalid in the identical means that HTML kind validation does:
enter.addEventListener("enter", () => {
if (enter.validity.legitimate) {
/* Enter is legitimate */
} else {
/* Enter is invalid (the invalid occasion doesn’t hearth) */
}
});
The factor about HTML kind validation is that it takes care of the whole entrance finish, but when there’s a non-default conduct that you simply want, checkValidity() or ValidityState is what you’re in search of.
The pseudo-classes will work both means. Slightly too nicely, actually! A simple factor to overlook is that kind controls set off both :legitimate or :invalid instantly. Nonetheless, :user-valid and :user-invalid anticipate customers to produce a worth and unfocus earlier than triggering. That is truly what the change occasion does (except the aspect is a checkbox, radio button, dropdown checklist, coloration picker, or vary slider), and what makes it completely different from the enter occasion.
There isn’t a JavaScript occasion for auto-filling or perhaps a clear option to detect it utilizing JavaScript, however there is an :autofill pseudo-class.
Media aspect pseudo-classes are nonetheless new. They aren’t supported by Chrome but and solely landed in Firefox lately, however they’re part of Interop 2026 and shortly we’ll be capable to type and parts primarily based on their state with out listening to JavaScript occasions. I’m certain you perceive how this works by now, so right here’s a fast rundown:
| Pseudo-class | JavaScript occasion equal |
|---|---|
:buffering |
ready |
:muted |
volumechange (however see beneath) |
:paused |
pause |
:enjoying |
enjoying (not play) |
:looking for |
looking for |
:stalled |
stalled |
:volume-locked |
N/A, see beneath |
Use the volumechange occasion to detect mute:
audio.addEventListener("volumechange", () => {
if (audio.muted) {
// Muted
} else {
// Not muted
}
});
Detecting quantity lock means making an attempt to vary the quantity and checking for fulfillment. The perfect strategy is to create a wholly new aspect in order that we don’t set off volumechange on the actual one:
// Create video
const video = doc.createElement("video");
// Change quantity
video.quantity = 0.5;
if (video.quantity !== 0.5) {
// Quantity locked
} else {
// Quantity not locked
}
(Or to make use of the :volume-locked pseudo-class, if writing CSS.)
:popover-open / :open / :modal
As we would anticipate, there’s no JavaScript occasion for when a popover, , or
toggle occasion after which examine the state:
aspect.addEventListener("toggle", () => {
if (aspect.open) {
/* Popover/dialog/particulars open */
} else {
/* Popover/dialog/particulars not open */
}
});
Nonetheless, CSS gives these pseudo-classes proper out of the field:
:popover-open(for popovers):open(forandparts)
:modal(for modals and fullscreen parts)
Talking of fullscreen parts…
:fullscreen
The :fullscreen pseudo-class is synonymous with the fullscreenchange JavaScript occasion with a conditional baked in:
doc.addEventListener("fullscreenchange", () => {
if (doc.fullscreenElement) {
/* fullscreenElement is fullscreen */
} else {
/* Nothing is fullscreen (fullscreenElement is null) */
}
});
:goal
When a URL hash (e.g., #contact) matches a component’s ID (e.g.,
:goal pseudo-class. When utilizing JavaScript, we have now to hear for the hashchange occasion after which see if an identical aspect is discovered:
window.addEventListener("hashchange", () => {
const goal = doc.getElementById(window.location.hash.substring(1));
if (goal) {
/* Matching aspect discovered */
} else {
/* Matching aspect not discovered */
}
});
Conclusion (however not likely)
This isn’t a “JavaScript unhealthy” rant however moderately an appreciation for what CSS simplifies with out forgetting the surgical management that JavaScript gives. Extra methods to do issues isn't a foul factor.
And on that observe, I wish to shortly point out event-trigger.
Precise occasion listeners (event-trigger)
I got here throughout occasion triggers when Chrome carried out scroll-triggered animations, as they’re in the identical module, however they’re not supported by any internet browser but, so if I make any errors, I apologize. Let’s dive in.
event-trigger-name will settle for a easy dashed ident:
button {
event-trigger-name: --event;
}
event-trigger-source would be the occasion listener, basically.
It’ll settle for the next key phrases:
activatecuriosityclick oncontactdblclickkeypress()
button {
event-trigger-source: click on;
}
I imagine the curiosity key phrase refers back to the upcoming Curiosity Invoker API whereas the activate key phrase may depend upon the aspect. For
Anyway, the occasions will set off animations. First we’d create a @keyframes animation, then we’d connect it to the aspect to be animated, however the animation wouldn’t run till triggered by the occasion (whereas usually they’d run instantly).
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
div {
animation: fade-in 300ms each;
}
Then we be certain that when the occasion fires, the animation triggers. We do that by setting animation-trigger alongside animation, referencing the dashed ident (--event). This has the non-obligatory good thing about permitting the occasion of 1 aspect to set off the animation of one other. Right here’s a fast instance, utilizing the event-trigger shorthand this time:
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
button {
/* On click on, set off --event animation */
event-trigger: --event click on;
}
div {
/* When --event fires, play animation forwards */
animation-trigger: --event play-forwards;
/* Animation */
animation: fade-in 300ms each;
}
That is what’s referred to as a statemuch less occasion set off. Give it some thought — you'll be able to’t unclick a click on, proper? However we will lose curiosity, so right here’s what a statefull event-triggered animation would appear like (discover the syntax for two occasions separated by a / and two animation actions, one for every state):
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
button {
/* curiosity (entry) / curiosity (exit) */
event-trigger: --event curiosity / curiosity;
}
div {
/* Play ahead with curiosity, backward when dropping it */
animation-trigger: --event play-forwards play-backwards;
/* Animation */
animation: fade-in 300ms each;
}
Acceptable animation actions embrace:
noneplayplay-onceplay-forwardsplay-backwardspauseresetreplay
There are lots of combos of occasions and animation actions that wouldn’t work, however these could be straightforward to sidestep as a result of it wouldn’t make sense to make use of them. We may, nonetheless, set off a number of completely different animations as a result of animation-trigger is a reset-only sub-property animation. Right here’s a tough instance:
animation-name: animationA, animationB;
animation-trigger: --eventA play, --eventB replay;
The chances are limitless relying on how the W3C transfer ahead with this characteristic (the spec mentions permitting for occasion effervescent!), however I kinda want we may invoke JavaScript strategies with occasion triggers like how HTML can with the Invoker Instructions API.
What do you suppose? A step in the proper course, or does CSS want to remain in its lane?
