惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
爱范儿
爱范儿
B
Blog
博客园 - 【当耐特】
A
Arctic Wolf
The Last Watchdog
The Last Watchdog
T
Tailwind CSS Blog
博客园 - Franky
Vercel News
Vercel News
P
Privacy International News Feed
F
Full Disclosure
Jina AI
Jina AI
H
Help Net Security
C
Cybersecurity and Infrastructure Security Agency CISA
T
Threat Research - Cisco Blogs
V
Visual Studio Blog
Y
Y Combinator Blog
GbyAI
GbyAI
K
Kaspersky official blog
P
Proofpoint News Feed
G
GRAHAM CLULEY
Security Latest
Security Latest
小众软件
小众软件
L
LINUX DO - 最新话题
S
Security @ Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Check Point Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园_首页
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
O
OpenAI News
大猫的无限游戏
大猫的无限游戏
Forbes - Security
Forbes - Security
P
Palo Alto Networks Blog
TaoSecurity Blog
TaoSecurity Blog
V
V2EX
D
DataBreaches.Net
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
M
MIT News - Artificial intelligence
博客园 - 叶小钗
美团技术团队
H
Hacker News: Front Page
云风的 BLOG
云风的 BLOG
WordPress大学
WordPress大学
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cloudbric
Cloudbric

CSS-Tricks

translateZ() | CSS-Tricks translateY() | CSS-Tricks translateX() | CSS-Tricks translate() | CSS-Tricks Using Scroll-Driven Animations for Opposing Scroll Directions | CSS-Tricks A First Look at Scroll-Triggered Animations | CSS-Tricks The Siren Song of  ariaNotify() | CSS-Tricks Prop For That | CSS-Tricks What’s !important #13: @function, alpha(), CSS Wordle, and More | CSS-Tricks There’s no need to include ‘navigation’ in your navigation labels | CSS-Tricks Why Isn't My 3D View Transition Working? | CSS-Tricks Creating Memorable Web Experiences: A Modern CSS Toolkit | CSS-Tricks Scroll-Driven, Scroll-Triggered, Scroll States, and View Transitions | CSS-Tricks Another Stab at the Perfect CSS Pie Chart... Sans JavaScript! | CSS-Tricks offset-path | CSS-Tricks @custom-media | CSS-Tricks @function | CSS-Tricks ::search-text | CSS-Tricks Astro Markdown Component Utility for Any Framework | CSS-Tricks What’s !important #12: Safari Testing, ::checkmark, HTML Anchor Positioning, and More | CSS-Tricks Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks hypot() | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks
The Shifting Line Between CSS States and JavaScript Events | CSS-Tricks
Daniel Schwarz · 2026-06-29 · via CSS-Tricks

CSS is listening to us. No, not like that. Rather, CSS is accumulating more and more pseudo-classes to help us respond to JavaScript events so that we don’t have to do so with JavaScript itself. But while pseudo-classes track states, not events, they sure can feel like event listeners sometimes (not that it really matters in the context of CSS).

Then again, what is CSS these days? For example, there’s a proposal for event-trigger in the Animation Triggers spec, which would basically listen for events and trigger animations. If you ask me though, the syntax is capable of a lot more than that (think: invoker commands but for CSS).

But to stay in today’s reality, I’ll walk you through the different CSS pseudo-classes out there that are kind of like event listeners, before doing the same for event-trigger, where I’ll show you how (I think) this currently unsupported feature would work.

“Event listening” pseudo-classes

:hover and :active

The :hover state captures the moment from when the pointerenter event fires to when the pointerleave event fires, which perfectly illustrates why pseudo-classes are states, not events.

:active matches the target (e.g., a link or button) that’s currently being pressed with a mouse, finger, or stylus, which makes it akin to pointerdown and pointerup/pointercancel.

By the way, the pointer-events: none CSS declaration prevents pointer events from firing on the selected element!

:focus and :focus-visible

The :focus pseudo-class is akin to the focus and blur (unfocus) JavaScript events, but :focus-visible is a bit more complex. :focus-visible triggers when :focus does, but in addition, the browser uses a variety of heuristics to determine whether or not a focus indicator should be shown. Is the user operating with a keyboard? Is the element a form control? This really makes me appreciate what CSS offers. In fact, the best way to handle this using JavaScript is to query the CSS pseudo-class:

element.addEventListener("focus", (event) => {
  if (event.target.matches(":focus-visible")) {
    /* Do something */
  }
});

:focus-within (and :has())

JavaScript excels at the “if A is Y, then do Z to B” kind of stuff. We can traverse the DOM, leverage event propagation, and much more. In that regard, CSS can feel a bit limited. However, CSS is evolving quickly. It has many new if-this-do-that features such as scroll-driven animations, and it’ll have more in the future. HTML is doing the same with dedicated components such as <details>, which all have accompanying CSS features.

I’ll mention some of that later, actually. In a more holistic sense, what we have is :focus-within, which matches if a child has focus, and :has(), which accepts any valid selector and matches if such a relationship exists between the two selectors.

For example, these two selectors do the exact same thing:

form:focus-within {
  /* Style the form when something within has focus */
}

form:has(:focus) {
  /* Style the form when something within has focus */
}

:checked

It’s fairly obvious what :checked does. The JavaScript event that’s most synonymous with it is change, which fires when the value of an <input>, <select>, or <textarea> changes (although, in this context, the input event is quite similar).

To listen for a check, we’d do something like this:

checkbox.addEventListener("change", (event) => {
  if (event.target.checked) {
    /* Checked */
  } else {
    /* Not checked */
  }
});

CSS pseudo-classes often capture the moment between two JavaScript events (e.g., pointerenter and pointerleave), but when they’re not doing that, they’re handling logic instead, as above.

Let’s look at some more examples of hidden logic handling.

:valid/:invalid/:user-valid/:user-invalid/:autofill

We don’t need the :not() pseudo-class function here, as validity can be checked using both the :valid and :invalid pseudo-classes, but on the JavaScript side of things, there’s no valid event (only invalid). That being said, if using JavaScript, you’ll likely want to call the checkValidity() method (which actually fires the invalid event if it returns false) within the callback of the event listener for input, change, blur (to check validity when unfocusing from an element), or submit (to check validity of the entire form when submitting it, as below).

form.addEventListener("submit", () => {
  if (form.checkValidity()) {
    /* All form controls are valid */
  } else {
    /* A form control is invalid (the invalid event fires) */
  }
});

We can also do this with the ValidityState object, which doesn’t fire the invalid event, but does tell us why a form control is valid or invalid in the same way that HTML form validation does:

input.addEventListener("input", () => {
  if (input.validity.valid) {
    /* Input is valid */
  } else {
    /* Input is invalid (the invalid event doesn’t fire) */
  }
});

The thing about HTML form validation is that it takes care of the entire front end, but if there’s a non-default behavior that you need, checkValidity() or ValidityState is what you’re looking for.

The pseudo-classes will work either way. A little too well, in fact! An easy thing to miss is that form controls trigger either :valid or :invalid immediately. However, :user-valid and :user-invalid wait for users to supply a value and unfocus before triggering. This is actually what the change event does (unless the element is a checkbox, radio button, dropdown list, color picker, or range slider), and what makes it different from the input event.

There isn’t a JavaScript event for auto-filling or even a clean way to detect it using JavaScript, but there is an :autofill pseudo-class.

Media element pseudo-classes are still new. They aren’t supported by Chrome yet and only landed in Firefox recently, but they are a part of Interop 2026 and soon we’ll be able to style <audio> and <video> elements based on their state without listening to JavaScript events. I’m sure you understand how this works by now, so here’s a quick rundown:

Pseudo-classJavaScript event equivalent
:bufferingwaiting
:mutedvolumechange (but see below)
:pausedpause
:playingplaying (not play)
:seekingseeking
:stalledstalled
:volume-lockedN/A, see below

Use the volumechange event to detect mute:

audio.addEventListener("volumechange", () => {
  if (audio.muted) {
    // Muted
  } else {
    // Not muted
  }
});

Detecting volume lock means trying to change the volume and checking for success. The best approach is to create an entirely new element so that we don’t trigger volumechange on the real one:

// Create video
const video = document.createElement("video");

// Change volume
video.volume = 0.5;

if (video.volume !== 0.5) {
  // Volume locked
} else {
  // Volume not locked
}

(Or to use the :volume-locked pseudo-class, if writing CSS.)

:popover-open / :open / :modal

As we might expect, there’s no JavaScript event for when a popover, <dialog>, or <details> opens or closes, but we can listen for the toggle event and then check the state:

element.addEventListener("toggle", () => {
  if (element.open) {
    /* Popover/dialog/details open */
  } else {
    /* Popover/dialog/details not open */
  }
});

However, CSS offers these pseudo-classes right out of the box:

  • :popover-open (for popovers)
  • :open (for <dialog> and <details> elements)
  • :modal (for modal <dialog>s and fullscreen elements)

Speaking of fullscreen elements…

:fullscreen

The :fullscreen pseudo-class is synonymous with the fullscreenchange JavaScript event with a conditional baked in:

document.addEventListener("fullscreenchange", () => {
  if (document.fullscreenElement) {
    /* fullscreenElement is fullscreen */
  } else {
    /* Nothing is fullscreen (fullscreenElement is null) */
  }
});

:target

When a URL hash (e.g., #contact) matches an element’s ID (e.g., <div id="contact">), that element matches the :target pseudo-class. When using JavaScript, we have to listen for the hashchange event and then see if a matching element is found:

window.addEventListener("hashchange", () => {
  const target = document.getElementById(window.location.hash.substring(1));

  if (target) {
    /* Matching element found */
  } else {
    /* Matching element not found */
  }
});

Conclusion (but not really)

This isn’t a “JavaScript bad” rant but rather an appreciation for what CSS simplifies without forgetting the surgical control that JavaScript offers. More ways to do things is never a bad thing.

And on that note, I want to quickly mention event-trigger.

Actual event listeners (event-trigger)

I came across event triggers when Chrome implemented scroll-triggered animations, as they’re in the same module, but they’re not supported by any web browser yet, so if I make any mistakes, I apologize. Let’s dive in.

event-trigger-name will accept a simple dashed ident:

button {
  event-trigger-name: --event;
}

event-trigger-source will be the event listener, essentially.

It’ll accept the following keywords:

  • activate
  • interest
  • click
  • touch
  • dblclick
  • keypress(<string>)
button {
  event-trigger-source: click;
}

I believe the interest keyword refers to the upcoming Interest Invoker API whereas the activate keyword could depend on the element. For <details> for example, activation could mean when opened, but I’m not sure. Subsequent drafts of the spec should tell us more, and reveal more events.

Anyway, the events will trigger animations. First we’d create a @keyframes animation, then we’d attach it to the element to be animated, but the animation wouldn’t run until triggered by the event (whereas normally they’d run immediately).

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

div {
  animation: fade-in 300ms both;
}

Then we ensure that when the event fires, the animation triggers. We do this by setting animation-trigger alongside animation, referencing the dashed ident (--event). This has the optional benefit of allowing the event of one element to trigger the animation of another. Here’s a quick example, using the event-trigger shorthand this time:

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

button {    
  /* On click, trigger --event animation */
  event-trigger: --event click;
}

div {
  /* When --event fires, play animation forwards */
  animation-trigger: --event play-forwards;

  /* Animation */
  animation: fade-in 300ms both;
}

This is what’s called a stateless event trigger. Think about it — you can’t unclick a click, right? But we can lose interest, so here’s what a statefull event-triggered animation would look like (notice the syntax for two events separated by a / and two animation actions, one for each state):

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

button {    
  /* interest (entry) / interest (exit) */
  event-trigger: --event interest / interest;
}

div {
  /* Play forward with interest, backward when losing it */
  animation-trigger: --event play-forwards play-backwards;

  /* Animation */
  animation: fade-in 300ms both;
}

Acceptable animation actions include:

  • none
  • play
  • play-once
  • play-forwards
  • play-backwards
  • pause
  • reset
  • replay

There are many combinations of events and animation actions that wouldn’t work, but these would be easy to sidestep because it wouldn’t make sense to use them. We could, however, trigger multiple different animations because animation-trigger is a reset-only sub-property animation. Here’s a rough example:

animation-name: animationA, animationB;
animation-trigger: --eventA play, --eventB replay;

The possibilities are endless depending on how the W3C move forward with this feature (the spec mentions allowing for event bubbling!), but I kinda wish we could invoke JavaScript methods with event triggers like how HTML can with the Invoker Commands API.

What do you think? A step in the right direction, or does CSS need to stay in its lane?