> All in One 586

Ads

Monday, June 29, 2026

The Shifting Line Between CSS States and JavaScript Events

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

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?


The Shifting Line Between CSS States and JavaScript Events originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



from CSS-Tricks https://ift.tt/i4gwJbo
via IFTTT

Sunday, June 28, 2026

Clear today!



With a high of F and a low of 61F. Currently, it's 75F and Clear outside.

Current wind speeds: 15 from the South

Pollen: 3

Sunrise: June 28, 2026 at 05:27PM

Sunset: June 29, 2026 at 08:21AM

UV index: 0

Humidity: 25%

via https://ift.tt/lxqzovs

June 29, 2026 at 10:02AM

Saturday, June 27, 2026

Clear/Wind today!



With a high of F and a low of 60F. Currently, it's 77F and Clear outside.

Current wind speeds: 15 from the South

Pollen: 3

Sunrise: June 27, 2026 at 05:27PM

Sunset: June 28, 2026 at 08:22AM

UV index: 0

Humidity: 31%

via https://ift.tt/BdYyjtU

June 28, 2026 at 10:02AM

Friday, June 26, 2026

Showers Early today!



With a high of F and a low of 61F. Currently, it's 67F and Clear outside.

Current wind speeds: 18 from the Southeast

Pollen: 3

Sunrise: June 26, 2026 at 05:27PM

Sunset: June 27, 2026 at 08:21AM

UV index: 0

Humidity: 84%

via https://ift.tt/vGS7t0z

June 27, 2026 at 10:02AM

Thursday, June 25, 2026

Cloudy today!



With a high of F and a low of 56F. Currently, it's 61F and Cloudy outside.

Current wind speeds: 13 from the Northeast

Pollen: 3

Sunrise: June 25, 2026 at 05:26PM

Sunset: June 26, 2026 at 08:21AM

UV index: 0

Humidity: 88%

via https://ift.tt/yjXfCTp

June 26, 2026 at 10:02AM

translate()

The CSS translate() function shifts an element from its default position on a two-dimensional plane. This way, we can reposition an element horizontally, vertically, or both.

.parent:hover .box {
  transform: translate(50px, 50%);
}

Hover over the box to see it move 50% of its width towards the left:

Along with other transform functions, it is used inside the transform property.

The translate() function is defined in the CSS Transforms Module Level 1 draft.

Syntax

The translate() function’s syntax looks like this:

<translate()> = translate( <length-percentage>, <length-percentage>? )

…which is just a fancy way of saying we can move (translate) and element by one or two lengths or percentages. We’ll get into some examples in a bit. But first:

Arguments

/* Single argument */
translate(100px) /* moves 100px to the right */
translate(-100%) /* moves 100% of the element's width to the left */

/* Double argument */
translate(50px, 100px) /* moves 50px down, then 100px to the right */
translate(50%, 100%) /* moves 50% of the element's width downwards, then 100% its height to the right */

The translate() function takes two <length-percentage> arguments (txty, as in “translate horizontally” and “translate vertically”). These tell the browser how much to move the element and in which direction direction (whether it’s positive or negative).

  • tx: Specifies the displacement in the horizontal axis. If it’s positive, the element goes right. If it’s negative, the element shifts to the left.
  • ty (optional): Specifies the displacement in the vertical axis. If it’s positive, the element goes downward, and if it’s negative, the element moves upward.

If only one argument is passed, it’s assumed to be tx. Alternatively, when both arguments are passed, the second argument will be ty. Together, they shift the element diagonally.

You’ll also notice that we can use either <length> or <percentage> values. A <length> value is absolute, while a <percentage> value is relative to the element’s width (for tx) or height (for ty).

Basic usage

While we have many ways to center an element in CSS, for most of its history, our best shot to center an absolute element was using the translate() function.

The process goes as follows: given an absolute element, we usually shift it to the center using top: 50% and left: 50%. However, these alone only fix the top-left corner of the element in the center, not the element itself. To fix this, we use transform: translate(-50%, -50%) to shift the element back by half of its own width and height.

.modal-center {
  position: absolute;
  top: 50%;
  left: 50%;

  transform: translate(-50%, -50%) scale(0.9);
}

More recently, we can do this using the known justify-self and align-self properties. Alternatively, we could have used the semantic dialog modal which is centered by default.

Diagonal movements

The translateX() function moves elements horizontally, while translateY() handles the vertical axis. If we instead want diagonal movement, we could combine both or use the shorter translate() function.

A common use case would be to translate an element into the page from any corner. For example, if we had a Toast component and wanted it to slide in from the bottom-right, we could move the element through the bottom and right properties, then offset them off the page with translate().

.toast {
  position: fixed;
  bottom: 30px;
  right: 30px;

  transform: translate(40px, 40px);
  transition: transform 0.28s ease;
}

Then, when a .show class is triggered, the translate() values are reset, causing them to slide in diagonally:

.toast.show {
  opacity: 1;
  transform: translate(0, 0);
}

It doesn’t affect other elements

The translate() function, like other transform functions, does not affect the document flow. Instead, it visually displaces the translated element to a new position without pushing the elements in its surroundings or the ones at the new position. Also, the space the element originally occupied remains reserved in the layout as if it hadn’t moved at all.

/* Translated element */
.translated {
  position: absolute;
  top: 0;
  left: 0;

  transform: translate(80px, 40px);
}

Notice how the “translated” element does not cause the Box 1 or Box 2 elements next to it to shift when it is moved.

Unlike margin, which can trigger reflows or shift neighboring elements, translate() only changes where the element is visually rendered.

Issues with pointer pseudo-classes

Using translate() directly on a pointer pseudo-classes like :hover can sometimes make for bad interactions. In a situation where the element is translated far enough from the cursor, the :hover state ends, causing the element to snap back immediately to its original position. A position where the cursor still lingers, so it translates again, resulting in a continuous flickering loop.

A simple solution to this is to place the element to be translated in a parent container, and apply the pseudo-class (:hover) to the parent, while the main element takes the translate function.

/* Problem case */
.bad:hover {
  transform: translateX(160px);
}

/* Solution */
.parent:hover .good {
  transform: translateX(160px);
}

Demo

Specification

The CSS translate() function is defined in the CSS Transforms Module Level 1, which is currently in Editor’s Drafts.

Browser support

The translate() function has baseline support on all modern browsers.

Further reading


translate() originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



from CSS-Tricks https://ift.tt/pMXysCE
via IFTTT

Wednesday, June 24, 2026

Scattered Strong Storms/Wind today!



With a high of F and a low of 57F. Currently, it's 71F and Clear outside.

Current wind speeds: 18 from the Southeast

Pollen: 3

Sunrise: June 24, 2026 at 05:26PM

Sunset: June 25, 2026 at 08:21AM

UV index: 0

Humidity: 64%

via https://ift.tt/6PVTSDu

June 25, 2026 at 10:02AM

The Shifting Line Between CSS States and JavaScript Events

CSS is listening to us. No, not like that . Rather, CSS is accumulating more and more pseudo-classes to help us respond to JavaScript event...