CSS is listening to us. No, not like that. Fairly, CSS is accumulating increasingly more 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 positive can really feel like occasion listeners generally (not that it actually issues within the context of CSS).
Then once more, what is CSS as of late? For instance, there’s a proposal for event-trigger within the Animation Triggers spec, which might principally pay attention for occasions and set off animations. If you happen to ask me although, the syntax is able to much more than that (assume: invoker instructions however for CSS).
However to remain in at present’s actuality, I’ll stroll you thru the completely different CSS pseudo-classes on the market which might be form of like occasion listeners, earlier than doing the identical for event-trigger, the place I’ll present you ways (I feel) this presently unsupported function 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 best way, 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 consumer working with a keyboard? Is the aspect a type management? This actually makes me respect what CSS presents. In reality, the easiest way 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” form of stuff. We are able to traverse the DOM, leverage occasion propagation, and far more. In that regard, CSS can really feel a bit restricted. Nonetheless, CSS is evolving rapidly. It has many new if-this-do-that options corresponding to scroll-driven animations, and it’ll have extra sooner or later. HTML is doing the identical with devoted parts corresponding to
I’ll point out a few of that later, truly. In a extra holistic sense, what we’ve got 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:
type:focus-within {
/* Fashion the shape when one thing inside has focus */
}
type: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 sort of related).
To pay attention for a test, we’d do one thing like this:
checkbox.addEventListener("change", (occasion) => {
if (occasion.goal.checked) {
/* Checked */
} else {
/* Not checked */
}
});
CSS pseudo-classes usually seize the second between two JavaScript occasions (e.g., pointerenter and pointerleave), however after they’re not doing that, they’re dealing with logic as a substitute, as above.
Let’s have 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 operate right here, as validity might be checked utilizing each the :legitimate and :invalid pseudo-classes, however on the JavaScript aspect of issues, there’s no legitimate occasion (solely invalid). That being mentioned, if utilizing JavaScript, you’ll possible wish to name the checkValidity() technique (which truly fires the invalid occasion if it returns false) throughout the callback of the occasion listener for enter, change, blur (to test validity when unfocusing from a component), or submit (to test validity of your complete type when submitting it, as under).
type.addEventListener("submit", () => {
if (type.checkValidity()) {
/* All type controls are legitimate */
} else {
/* A type management is invalid (the invalid occasion fires) */
}
});
We are able to additionally do that with the ValidityState object, which doesn’t hearth the invalid occasion, however does inform us why a type management is legitimate or invalid in the identical method that HTML type 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 type validation is that it takes care of your complete entrance finish, but when there’s a non-default conduct that you just want, checkValidity() or ValidityState is what you’re searching for.
The pseudo-classes will work both method. Slightly too nicely, in actual fact! A simple factor to overlook is that type controls set off both :legitimate or :invalid instantly. Nonetheless, :user-valid and :user-invalid look ahead to customers to provide a price and unfocus earlier than triggering. That is truly what the change occasion does (until the aspect is a checkbox, radio button, dropdown record, colour 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 approach 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 fashion and parts primarily based on their state with out listening to JavaScript occasions. I’m positive 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 under) |
:paused |
pause |
:enjoying |
enjoying (not play) |
:in search of |
in search of |
:stalled |
stalled |
:volume-locked |
N/A, see under |
Use the volumechange occasion to detect mute:
audio.addEventListener("volumechange", () => {
if (audio.muted) {
// Muted
} else {
// Not muted
}
});
Detecting quantity lock means attempting to alter the amount and checking for achievement. One of the best strategy is to create a completely 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’d count on, there’s no JavaScript occasion for when a popover, , or
toggle occasion after which test the state:
aspect.addEventListener("toggle", () => {
if (aspect.open) {
/* Popover/dialog/particulars open */
} else {
/* Popover/dialog/particulars not open */
}
});
Nonetheless, CSS presents 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've got to pay attention 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 probably not)
This isn’t a “JavaScript unhealthy” rant however moderately an appreciation for what CSS simplifies with out forgetting the surgical management that JavaScript presents. Extra methods to do issues isn't a foul factor.
And on that be aware, I wish to rapidly point out event-trigger.
Precise occasion listeners (event-trigger)
I got here throughout occasion triggers when Chrome applied scroll-triggered animations, as they’re in the identical module, however they’re not supported by any net 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, primarily.
It’ll settle for the next key phrases:
activatecuriosityclick oncontactdblclickkeypress()
button {
event-trigger-source: click on;
}
I consider the curiosity key phrase refers back to the upcoming Curiosity Invoker API whereas the activate key phrase may rely 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 sure 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 known 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 seem 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 shedding it */
animation-trigger: --event play-forwards play-backwards;
/* Animation */
animation: fade-in 300ms each;
}
Acceptable animation actions embrace:
noneplayplay-onceplay-forwardsplay-backwardspauseresetreplay
There are a lot of combos of occasions and animation actions that wouldn’t work, however these can 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 function (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 assume? A step in the suitable course, or does CSS want to remain in its lane?




![How creators and entrepreneurs are utilizing AI to hurry up & succeed [data]](https://blog.aimactgrow.com/wp-content/uploads/2025/06/Untitled20design-Apr-07-2023-08-24-35-4586-PM-120x86.png)



