> All in One 586: June 2026

Ads

Tuesday, June 30, 2026

Thunderstorms/Wind Early today!



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

Current wind speeds: 18 from the Southeast

Pollen: 3

Sunrise: June 30, 2026 at 05:28PM

Sunset: July 1, 2026 at 08:21AM

UV index: 0

Humidity: 44%

via https://ift.tt/D3HRLg5

July 1, 2026 at 10:02AM

Monday, June 29, 2026

Clear today!



With a high of F and a low of 55F. Currently, it's 70F and Clear outside.

Current wind speeds: 13 from the Northeast

Pollen: 3

Sunrise: June 29, 2026 at 05:28PM

Sunset: June 30, 2026 at 08:22AM

UV index: 0

Humidity: 50%

via https://ift.tt/V81HhuN

June 30, 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 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

Tuesday, June 23, 2026

Strong Storms today!



With a high of F and a low of 58F. Currently, it's 68F and Showers in the Vicinity outside.

Current wind speeds: 14 from the Southeast

Pollen: 0

Sunrise: June 23, 2026 at 05:26PM

Sunset: June 24, 2026 at 08:21AM

UV index: 0

Humidity: 81%

via https://ift.tt/X9IilhL

June 24, 2026 at 10:02AM

Monday, June 22, 2026

Partly Cloudy/Wind today!



With a high of F and a low of 59F. Currently, it's 65F and Fair/Wind outside.

Current wind speeds: 20 from the Southeast

Pollen: 3

Sunrise: June 22, 2026 at 05:25PM

Sunset: June 23, 2026 at 08:21AM

UV index: 0

Humidity: 86%

via https://ift.tt/DXvIzkx

June 23, 2026 at 10:02AM

Using Scroll-Driven Animations for Opposing Scroll Directions

Sometimes designers have silly ideas that eventually grow on you. That happened to me with this concept where I had to build columns of items moving in opposite directions when a user scrolls the page.

Note: This demo respects reduced motion settings, so you’ll need to enable motion to see the effect. And we’re looking at Chrome and Safari support as I’m writing this.

It’s really not as hard as you might think, thanks to modern CSS features, specifically scroll-driven animations. Mot only that, but it’s fun to make, too! Let me show you how I approached it — and maybe you will want to share how you would do it differently.

The HTML

The HTML consists of a parent element (.opposing-columns), its children (.opposing-column), and its children’s children (.opposing-item):

<div class="opposing-columns">
  <!-- Column 1 -->
  <div class="opposing-column">
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
  </div>
  <!-- Column 2 -->
  <div class="opposing-column">
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
  </div>
  <!-- Column 3 -->
  <div class="opposing-column">
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
    <div class="opposing-item">...</div>
  </div>
</div>

This is all we need in the markup. CSS will do the rest!

Styling the parent container

First off, we’re going to set things up so that this effect only applies to larger screens — there’s no real sense supporting something like this on smaller screens because we need the additional space for the effect.

/* Just on larger screens */
@media screen and (width >= 50rem) {
  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 50rem);
    margin-inline: auto;
  }
}

Setting up a “masking” effect

We need to do a few more things with the parent container to get the illusion that items in each .opposing-column are disappearing as they scroll past it. The items in the outer columns move upward on scroll, and items in the center column move downward. As they cross the parent’s boundaries, we want them to sorta fade out.

So, we’re going to do a few things. First, we’ll set a background color variable on the document as a whole:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    /* same styles as before */
  }
}

Second, we’ll apply that same background color on the parent’s :before and :after pseudo-elements:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    /* same styles as before */
  
    &:before,
    &:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}

Notice that we’ve established a stacking context on the pseudos and set them one layer above the parent and its descendants. This is key for masking the items in each column as they scroll in and out of the container. Thew items are technically sliding under the pseudo masks.

Speaking of which, let’s create another variable called --opposing-mask that adds vertical space between the parent element and the three columns:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }
  
  .opposing-columns {
    display: flex;
    gap: 2rem;
    max-inline-size: min(90dvi, 50rem);
    margin-inline: auto;
    margin-block: var(--opposing-mask, 3rem);
    position: relative;
  }
}
Highlighting the vertical space between the parent container and its child elements.

Let’s do the same thing to the parent’s pseudos, only applying --opposing-mask to their block-size by a multiple of three. This way, there’s additional vertical space between them and the parent.

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    background-color: var(--opposing-bg);
  }

  .opposing-columns {
    /* same styles as before */
  
    &:before,
    &:after {
      content: "";
      position: absolute;
      inset-inline: 0;
      block-size: calc(var(--opposing-mask) * 3);
      pointer-events: none;
      z-index: 1;
    }
  }
}
Highlighting the vertical space between the parent container and its before pseudo element.

You might see where this is going. We have a nice amount of space between the parent container and its pseudos. We want the column items to appear as if they are fading out as they scroll out of the parent container. We don’t have to mess with their opacity or anything like that. Instead, we can add background gradients on the pseudos.

The :before pseudo is at the top of the container, so we’ll give it a gradient that goes from a solid color that matches the document’s underlying background color to transparent, top-to-bottom. And since the :after pseudo sits at the bottom of the parent container, we’ll reverse the gradient so it goes transparent to the document’s background color, bottom-to-top.

@media screen and (width >= 50rem) {
  :root {
    /* same styles as before */
  }
  
  .opposing-columns {
      /* same styles as before */
    
      &:before,
      &:after {
        /* same styles as before */
      }
      
      &:before {
        background-image: linear-gradient(
          to bottom,
          var(--opposing-bg) var(--opposing-mask),
          transparent
        );
        inset-block-start: calc(var(--opposing-mask) * -1);
      }

      &:after {
        background-image: linear-gradient(
          to top,
          var(--opposing-bg) var(--opposing-mask),
          transparent
        );
        inset-block-end: calc(var(--opposing-mask) * -1);
      }
    }
  }
}

The column layouts

Before we get to the magic, we ought to lay out the items in each column. Each column is a flex item inside the parent, which is a flex container. We’ll let them shrink (flex-shrink: 1) and grow (flex-grow: 1), capping the size at a certain point (flex-basis: 10rem).

We can define all that with the flex shorthand property:

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    flex: 1 1 10rem;
  }
}

Now I want those columns to be grid containers so I can use the gap property to insert space between items:

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    flex: 1 1 10rem;
    display: grid;
    gap: 2rem;
  }
}

We totally could have used Flexbox here as well to get access to gap, but the default layout is set to row and we’d have to override that to column. Grid is a little more concise in this situation.

The animation!

This is what you came for, right? We’ve set everything up so that column items can flow in and out of the parent container on scroll. Now we need to add that scrolling behavior.

This is where the animation-timeline property comes real handy. Normally, a CSS animation just runs on its own. It starts when the page loads (or after a specific delay you set) and ends after however long you set the duration. With animation-timeline, we tell the animation to run based on its scroll position… hence the term “scroll-driven” animation.

We have two supported functions here, scroll() and view(). They’re related but super different in that scroll() runs the animation based on an element’s scroll position. The view() function is similar, but tracks the element’s progress as it enters and exits the scrollport (i.e., the scrollable area of the container it is in).

We’re going with the view() function because we’ve set this up where there is a clear scrollable area inside the parent container. We need to run the animation based on where it enters and exits that area rather than the scroll position of the column items.

This is real interesting because we can tell view() where exactly we want the animation to start once it enters the scrollable area and where to stop once it exits that same area. Like this:

/* Official syntax */
animation-timeline:  view([ <axis> || <'view-timeline-inset'>]?);

Let’s start by defining the axes:

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timeline: view();
    animation-range: entry cover;
  }
}

This is just partially what we want, but what we’re saying is we want the animation to (1) start the very moment is enters the scrollport (entry), and (2) end when it completely leaves the area (cover). We need to be explicitly about the insets because that’s what establishes the animation’s range relative to where it enters and exits. We want the full range, so the entry begins at 0% and the exit is when an item is covered at 100%.

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}

Lastly, we’ll set the animation to run linearly — no need for the items to slow up or down as they scroll.

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* ... */
    animation-timing-function: linear;
    animation-timeline: view();
    animation-range: entry 0% cover 100%;
  }
}

OK, great. But what we haven’t done is create an animation. We’ve set up what we want it to do when it runs, but we need to define the actual movement.

I want to set up three separate CSS animations:

  1. One that translates (moves) the items upward in the first column.
  2. One that’s the reverse of the first animation for the items in the other column.

We could technically set the first animation on both of the outer columns, but I want a third one that is a little bit offset from the first so those columns appear staggered.

@keyframes scroll1 {
  from { transform: translateY(var(--opposing-mask)); }
  to { transform: translateY(calc(var(--opposing-mask) * -1)); }
}

@keyframes scroll2 {
  from { transform: translateY(calc(var(--opposing-mask) * -1)); }
  to { transform: translateY(var(--opposing-mask)); }
}

@keyframes scroll3 {
  from { transform: translateY(calc(var(--opposing-mask) * .66)); }
  to { transform: translateY(calc(var(--opposing-mask) * -.33)); }
}

We can create variables for these, of course, should we ever need to update them:

@media screen and (width >= 50rem) {
  :root {
    --opposing-bg: lightcyan;
    --opposing-mask: 3rem;
    --animation-1: scroll1;
    --animation-2: scroll2;
    --animation-3: scroll3;

    /* ... */
  }
}

…and apply them to each column:

@media screen and (width >= 50rem) {
  /* same styles as before */

  .opposing-column {
    /* same styles as before */
  }

  :where(.opposing-column:nth-of-type(1)) {
    animation-name: var(--animation-1);
  }
  
  :where(.opposing-column:nth-of-type(2)) {
    animation-name: var(--animation-2);
  }

  :where(.opposing-column:nth-of-type(3)) {
    animation-name: var(--animation-3);
  }
}

While we’re at it, we should disable the animations to respect the user’s settings for reduced motion (and remove the mask, otherwise it might look weird):

@media (prefers-reduced-motion: reduce) { 
  .opposing-column {
    animation: unset;

    &:before,
    &:after {
      content: unset;
    }
  }
}

Wrapping up

So yeah, scroll-driven animations are really, really cool. We’re still waiting for Firefox support as I’m writing this, but you can certainly wrap this in @supports to provide a default experience that uses thew scroll annotations and then set a fallback experience for non-supporting browsers, like running on a normal animation timeline:

@supports (animation-timeline: view()) {
  /* ... */
}

This is just toe-dipping into what scroll-driven animations can do, of course. What sort of things have you made or experimented with? Or would you approach this one differently? Let me know!


Using Scroll-Driven Animations for Opposing Scroll Directions originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Sunday, June 21, 2026

Mostly Clear today!



With a high of F and a low of 52F. Currently, it's 61F and Fair outside.

Current wind speeds: 10 from the Northeast

Pollen: 3

Sunrise: June 21, 2026 at 05:25PM

Sunset: June 22, 2026 at 08:21AM

UV index: 0

Humidity: 84%

via https://ift.tt/FmcOGzI

June 22, 2026 at 10:02AM

Saturday, June 20, 2026

Partly Cloudy today!



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

Current wind speeds: 16 from the East

Pollen: 0

Sunrise: June 20, 2026 at 05:25PM

Sunset: June 21, 2026 at 08:21AM

UV index: 0

Humidity: 87%

via https://ift.tt/2P7h6Wc

June 21, 2026 at 10:02AM

Friday, June 19, 2026

Thunderstorms Early today!



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

Current wind speeds: 14 from the East

Pollen: 3

Sunrise: June 19, 2026 at 05:25PM

Sunset: June 20, 2026 at 08:20AM

UV index: 0

Humidity: 51%

via https://ift.tt/pD8zc0S

June 20, 2026 at 10:02AM

A First Look at Scroll-Triggered Animations

Chrome has shipped scroll-triggered animations, and is the first browser to do so. If you update to Chrome 146, you can view the demo below, where the background of a square fades in over the duration of 300ms, but only once the whole element is within the viewport.

This is a bit different to how scroll-driven animations work, so in this article I’ll compare them, and then show you how scroll-triggered animations work.

Scroll-triggered animations vs. scroll-driven animations

Scroll-triggered animations play for a fixed duration once a certain scroll threshold has been surpassed. (Think JavaScript’s Intersection Observer API but for CSS animations.)

This differs from scroll-driven animations, where animation progression is synchronized with scroll progression (animation-timeline: scroll()) or the degree of intersection (animation-timeline: view()), and thus has no duration.

Basic scroll-triggered animation example

The key part is timeline-trigger: view() instead of animation-timeline: view(), which waits for the element to be within the threshold instead of measuring how much it’s within it and doing something accordingly. However, let’s start with the actual @keyframes animation, which sets the background:

/* Define the animation */
@keyframes fade-bg-in {
  to {
    background: currentColor;
  }
}

It’s set on the .square over the duration of 300ms:

.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;
}

By default, CSS animations trigger when the declaration is applied, but in the expanded snippet below, timeline-trigger overwrites that behavior. Now the animation triggers when the element comes into view(). The --trigger is simply a dashed ident that acts as an identifier for the trigger, whereas entry 100% exit 0% is a timeline range. A timeline range specifies the scroll zone in which the animation activates and is allowed to remain active.

In this case, the animation triggers when the bottom edge of the .square enters the (entry 100%) and untriggers (assuming that it’s still running) when the top edge exits the scrollport (exit 0%). For clarity, entry 0% would trigger the animation when the top edge enters. entry handles the element coming in from the bottom of the scrollport, whereas exit handles it leaving through the top. It’s a bit confusing, but it’s easier to understand if I don’t over-explain it.

.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;

  /* Animation trigger conditions */
  timeline-trigger: --trigger view() entry 100% exit 0%;
}

For animation-trigger, we first specify which trigger we’re talking about, and then we declare some settings (e.g., play-forwards):

.square {
  /* Declare animation */
  animation: fade-bg-in 300ms;

  /* Animation trigger conditions */
  timeline-trigger: --trigger view() entry 100% exit 0%;

  /* Animation trigger settings */
  animation-trigger: --trigger play-forwards;
}

The play-forwards keyword triggers the animation whenever the square becomes completely visible, and since we haven’t declared a fill mode for the animation (using animation-fill-mode or as part of the animation shorthand), which means that the square won’t retain the background after, the animation is more of a flash.

So, we need to build upon this to achieve different results.

animation-fill-mode vs. <animation-action>

First, a recap of what the different fill mode values do for animation-fill-mode or as part of the animation shorthand:

  • forwards: the styles are retained after the animation.
  • backwards: the styles are applied before the animation.
  • both: both behaviors are applied.

Now, let’s assume that the <animation-action> is play-forwards (like before) and the fill mode is forwards (both would be redundant because background isn’t even set to begin with):

.square {
  animation: fade-bg-in 300ms forwards;
  timeline-trigger: --trigger view() entry 100% exit 0%;
  animation-trigger: --trigger play-forwards;
}

This causes the styles to be retained, but, if the square partially or completely exits the viewport and then reenters it, the animation restarts, which can cause a flash depending on how the animation ends, which is what happens in this instance.

There are two different ways to solve this…

The “lock-in” method: Use play-once instead of play-forwards, which, when combined with forwards, results in the animation playing once, never to restart, and then retaining the styles afterward.

.square {
  /* Play once */
  animation-trigger: --trigger play-once;

  /* Retain the styles */
  animation: fade-bg-in 300ms forwards;

  timeline-trigger: --trigger view() entry 100% exit 0%;
}

The “back-and-forth” method: play-forwards play-backwards animates the element normally when fully visible and in reverse when no longer fully visible. There’s no flash because the element animates backward as smoothly as it animates forward. In addition, even though the direction of the animation can change, the fill mode can remain at forwards instead of being set to both.

Why?

play-forwards means “play the animation from 0% to 100%” whereas play-backwards means “play the animation from 100% to 0%.” Meanwhile, as I mentioned earlier, the forwards fill mode means “retain the styles when the animation completes’”— well, this is regardless of whether the final keyframe is 0% or 100%.

.square {
  /* Play forward and backward, as appropriate */
  animation-trigger: --trigger play-forwards play-backwards;

  /* Retain the styles either way */
  animation: fade-bg-in 300ms forwards;

  timeline-trigger: --trigger view() entry 100% exit 0%;
}

play-forwards, play-once, and play-backwards aren’t the only keywords for <animation-action>. Here’s a quick rundown:

<animation-action>Effect
noneFor disabling triggers conditionally, on entry but not exit (or vice-versa), or handling multiple triggers with one animation-trigger
play-forwardsAllows the animation to play forward
play-backwardsAllows the animation to play backward
play-onceForward or backward (whichever comes first)
playPlays in the last specified direction, or forward if neither has been specified
pausePauses the animation
resetPauses the animation and sets progress to 0
replaySets progress to 0 but doesn’t pause the animation

These <animation-action>s not only allow for a significant amount of control over animations while scrolling, but different combinations of actions, fill modes, timeline ranges, and the fact that we can bake exit animations into @keyframes rules means that there are often multiple ways to achieve an outcome.

Scroll-triggering multiple elements

While scroll-triggered animations being made up of animation actions, fill modes, timeline ranges, and maybe more, might seem overcomplicated, the fact that these mechanics are decoupled enable us to reuse logic while maintaining flexibility, reducing repetition and making the mechanics more design system-friendly.

Consider three squares this time, and for a bit of added complexity, we declare scale: 70% (animates to initial) and define two rotative animations.

<div id="squares">
  <div class="square rotate-left"></div>
  <div class="square"></div>
  <div class="square rotate-right"></div>
</div>
/* Define animations */
@keyframes intensify {
  to {
    scale: initial;
    background: currentColor;
  }
}

@keyframes rotate-left {
  to {
    rotate: -5deg;
  }
}

@keyframes rotate-right {
  to {
    rotate: 5deg;
  }
}

.square {
  /* Set starting value */
  scale: 70%;
}

After that it’s more of the same, and while it’s obviously a more complex example, being able to merge values into shorthand properties and decouple them into longhand properties, as well as the decoupled nature of the different mechanics, facilitates flexibility but also reusability (in this case, to stagger various animations using the same animation trigger settings):

.square {
  /* Set starting value */
  scale: 70%;

  /* Define animation name */
  --base-animation: intensify;

  /* Declare animation */
  animation: var(--base-animation) 300ms forwards;

  /* Define animation trigger settings */
  --animation-trigger: --trigger play-forwards play-backwards;

  /* Declare for intensify, then for one of either rotate animations */
  animation-trigger: var(--animation-trigger), var(--animation-trigger);

  /* Declare animation trigger conditions (without timeline ranges) */
  timeline-trigger: --trigger view();

  /* Declare active range end */
  timeline-trigger-active-range-end: normal;

  /* Append other animations */
  &.rotate-left {
    animation-name: var(--base-animation), rotate-left;
  }

  &.rotate-right {
    animation-name: var(--base-animation), rotate-right;
  }

  /* Stagger activation ranges */
  &:first-child {
    timeline-trigger-activation-range-start: entry 33.3333%;
  }

  &:nth-child(2) {
    timeline-trigger-activation-range-start: entry 66.6666%;
  }

  &:last-child {
    timeline-trigger-activation-range-start: entry 99.9999%;
  }
}

Here’s a cleaner, more robust version that uses sibling-count() and sibling-index() (which lack Firefox support) to stagger the animations:

In this version, instead of setting timeline-trigger-activation-range-start on each individual square, we simply target .square and calculate the entry values on the fly:

/* Maximum entry ÷ number of squares */
--stagger-interval: calc(100% / sibling-count());

/* Current square’s index × stagger interval */
--entry: calc(sibling-index() * var(--stagger-interval));

/* Declare animation trigger conditions */
timeline-trigger: --trigger view() entry var(--entry) exit 0%;

Making one element trigger other elements

In this case, we’ll shift the trigger and its ranges to the first square, and have the other squares follow according to a staggered animation delay. As you can see, all animations are triggered by animation-trigger once 50% of the first square has entered (entry 50%) the viewport (view()). animation-trigger is triggered by timeline-trigger because the dashed ident (the aptly named --trigger) links them:

/* Define animations */
@keyframes intensify {
  to {
    scale: initial;
    background: currentColor;
  }
}

@keyframes rotate-left {
  to {
    rotate: -5deg;
  }
}

@keyframes rotate-right {
  to {
    rotate: 5deg;
  }
}

.square {
  /* Set starting value */
  scale: 70%;

  /* Define animation name */
  --base-animation: intensify;

  /* Maximum delay ÷ number of squares */
  --stagger-interval: calc(300ms / sibling-count());

  /* Current square’s index × stagger interval */
  --animation-delay: calc(sibling-index() * var(--stagger-interval));

  /* Declare animation */
  animation: var(--base-animation) 300ms var(--animation-delay) forwards;

  /* Define animation trigger settings */
  --animation-trigger: --trigger play-forwards play-backwards;

  /* Declare for intensify, then for one of either rotate animations */
  animation-trigger: var(--animation-trigger), var(--animation-trigger);

  &:first-child {
    /* Declare animation trigger conditions */
    timeline-trigger: --trigger view() entry 50%;

    /* Declare active range end */
    timeline-trigger-active-range-end: normal;
  }

  /* Append other animations */
  &.rotate-left {
    animation-name: var(--base-animation), rotate-left;
  }

  &.rotate-right {
    animation-name: var(--base-animation), rotate-right;
  }
}

One downside is that when animation-trigger is in play-backwards mode, the animations don’t stagger. This is because, I think, when the animation is reversed, the delay is included in that. This seems like an oversight to me, especially as that isn’t the case with animation-direction: reverse, but I could be completely wrong on this.

Understanding timeline ranges

Timeline ranges are a big part of scroll-triggered animations, but they’re a separate mechanic. For scroll-driven animations, you’ll want animation-range and its longhand properties. With scroll-triggered animations, the syntax is fundamentally the same but uses different properties and two different ranges. The activation range determines the scroll zone in which the animation triggers, while the active range determines the zone in which it holds up (even if not in the activation range anymore).

Timeline ranges are a bit heavy. However, view() entry 100% exit 0% (when fully visible) and view() contain (the same but also if larger than the viewport) will suffice most of the time.

But if you’re keen to dive in, animation-range, although it’s for scroll-driven animations, is lighter and offers a novice-level understanding of timeline ranges. After that, I recommend reading the Animation Triggers spec to cover the many intricacies of timeline ranges within the context of these scroll-triggered animations.

Another ingredient of scroll-triggered animations that’s also its own thing is the view() function, but this one’s easier to summarize here. Basically, when it comes to scroll-triggered animations, view() is the viewport. So if you had a 5rem sticky header, view(y 0 5rem) would make the timeline range factor that in along the y-axis.

Final thoughts

Scroll-triggered animations can be tricky because they’re similar to scroll-driven animations, they leverage older CSS features (mainly animation) as well as mechanics from other newer features (dashed idents, view(), timeline ranges), in addition to the CSS properties that are specific to scroll-triggered animations. There’s a whole lot happening at once.

I’m not sure how I feel about them, to be honest. They’re definitely cool, fun, and useful, but they’re also complicated, and it’ll be a while before I really start to rave about them.


A First Look at Scroll-Triggered Animations originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Thursday, June 18, 2026

Mostly Clear today!



With a high of F and a low of 54F. Currently, it's 68F and Clear outside.

Current wind speeds: 15 from the Southeast

Pollen: 0

Sunrise: June 18, 2026 at 05:25PM

Sunset: June 19, 2026 at 08:20AM

UV index: 0

Humidity: 36%

via https://ift.tt/Qw38EuJ

June 19, 2026 at 10:02AM

Wednesday, June 17, 2026

Mostly Clear today!



With a high of F and a low of 53F. Currently, it's 70F and Clear outside.

Current wind speeds: 15 from the Northeast

Pollen: 3

Sunrise: June 17, 2026 at 05:24PM

Sunset: June 18, 2026 at 08:20AM

UV index: 0

Humidity: 37%

via https://ift.tt/3tsZBRI

June 18, 2026 at 10:02AM

The Siren Song of  ariaNotify()

I need you all to promise me you’ll be cool about this. I‘m here to tell you about an upcoming web platform feature that has been a long time coming; a feature that not only fulfills a use case sorely overdue for a better solution, but does so by way of a syntax that is both immediately understandable and deceptively powerful. That’s right, this thing is developer catnip, and I don’t mind saying that I was really excited to try it out — after which point I willed myself to tuck it away in a drawer and put it out of my mind. This is a tool only to be used in situations where it is absolutely, one hundred percent necessary, to solve a problem that cannot be solved in any other way, up to and including “push back against building a feature in the first place.” So just be cool about this, okay? Okay.

There’s a brand new ariaNotify() method — defined by the Accessible Rich Internet Applications (WAI-ARIA) 1.3 Specification — that provides you with a means of programmatically triggering narration in a screen reader. It accepts a string as its first argument, and an optional configuration object as its second:

document.ariaNotify( "Hello, World." );
// When invoked, a screen reader will narrate "Hello, World."

That might look like a simple solution to an equally simple use case here in print, but historically this has a tricky problem that could only be solved by slightly off-label usage of ARIA’s live regions. That means that understanding live regions — and their shortcomings — is the key to understanding what ariaNotify does for us. If you’ve worked with live regions before, you likely closed this tab right after that code snippet, and you’re currently on your third or fourth lap around the room with your arms held aloft in triumph. If you haven’t worked with live regions before, well, to put it in the strictest possible technical terms: woof what a mess.

In an assisted browsing context, if some part of a page changes in response to a user interaction, or something is loaded and added to the page asynchronously, those changes aren’t discoverable until the user moved their focus to that changed content — a user would have no way of knowing that something had changed, let alone what. Live regions address that, at least by design: an element with an aria-live attribute will prompt narration for changes to the markup contained within that element — when the markup is changed, the changed markup is narrated aloud. If aria-live has a value of assertive, it informs assistive technology “this is urgent, and should be narrated right away.” If aria-live has a value of polite, it says “this should be narrated, at the next natural opportunity to do so.” Using role="alert" or role="status" on an element is functionally equivalent to aria-live="assertive" and aria-live="polite", respectively. Sounds pretty reasonable on paper, right?

Naturally, we needed a way to fine-tune exactly what information is narrated and how, so there are a few other attributes that determine a live region’s behavior:

aria-atomic

  • true: announce only the text that changes within the element
  • false (default): narrate the entire contents of the live region when something in it changes.

aria-relevant

  • text: notify the user when phrasing content — text, just like it says on the tin — changes inside the live region
  • additions: notify the user when a node is added to the live region
  • removals: notify the user when a node is removed from the live region
  • all (default): notify the user if text is changed, and/or elements are added to or removed from the DOM

Again, solid enough in theory! Problem solved, right up until the point where you try to use live regions, for pretty much anything, ever. In practice, browsers and assistive technologies are wildly inconsistent about implementation, particularly as it relates to nested markup within a live region — if you want aria-live to work as expected, you’ll often end up needing to strip out all the otherwise semantically-meaningful markup that you should be using. In order to work reliably across assisted browsing contexts, a live region has to already meaningfully exist in the DOM at the time the narration is triggered. A live region can’t be toggled from display: none or injected into the page along with the content to be narrated or you’ll run into timing issues that prevent the content from being narrated — when the browser first “sees” the live region, it locks in “okay, narrate anything that changes in this container,” which doesn’t necessarily include that initial content. The way the "assertive" and "polite" values work isn’t especially well-defined in the specification or realized across screen readers and browser combinations, either. Again: they’re a mess.

Even if all that weren’t the case, there’s a fundamental mismatch between the purpose of live regions and the way the modern web is built. Like I said, live regions only work when markup is added to/removed from the element in question, and that isn’t the reality of most interactions that result in a change to the visible contents of a page. Live regions are no help when you’re revealing markup that’s already in the document, but otherwise inert — for example, swapping between a visible display property and display: none. That use case is every bit as common as structural changes to the current document on-the-fly, if not more so.

All these limitations have led to live regions almost exclusively being used as makeshift notification APIs: having one or more aria-live elements buried in the page, visually hidden (but not removed from the accessibility tree via display: none), that you update as-needed with whatever text you want narrated. I have been there and done this, and it’s clunky, not least of all because of how inconsistent live regions are at their core. That injected content is also necessarily available to a user navigating through the page via assistive tech, just floating in the document divorced from it’s original meaning — if you’re not meticulous about cleaning up afterwards, you’ve added a potentially confusing source of contextually-irrelevant narration to the page. Most of all, though, you’ve added a new and invisible concern; a feature that will need dedicated testing and upkeep, and something that can break in very literally unseen ways, and do so in ways that have the potential to be annoying, misleading, confusing, or all of the above. That’s what gets you, with accessibility work: quick-and-easy decisions made in isolation can have unforeseen consequences in the context of the overall experience, and unless those assumptions are tested very carefully — early and often — we can’t know what those consequences might be.

The ariaNotify method takes the place of this kind of Rube Goldberg accessibility contraption, providing you with a real screen reader notification API, no convoluted (and flaky) markup required:

document.ariaNotify( "Hello, World." );

ariaNotify is available as a method on the Element interface or on the Document interface — there’s no meaningful difference between the two for the examples you’re going to see here, but it’s worth knowing that calling document.ariaNotify() means that the lang attribute specified on the html element, specifically, will be used to infer the language of the notification:

const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ) {
  document.ariaNotify( "Hello, World." );
});
/*
* Clicking the button results in the "polite"-timed announcement "hello, world," 
* using the `lang` attribute specified on the `<html>` element. If there isn't 
* one, the browser's default language is used.
/*

While calling ariaNotify() from an element means that the lang attribute of the element’s nearest ancestor will be used to determine the language of the notification:

const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ) {
  this.ariaNotify( "Hello, World." );
});
/*
* Clicking the button results in the "polite"-timed announcement "hello, world," 
* using the `lang` attribute of the `button` (or the closest parent element with 
* `lang`) to  determine pronunciation. If there isn't one in the document (all
* the way up to and including `<html>`), the browser's default language is used.
/*

ariaNotify accepts a second parameter that allows you to set an explicit priority level:

const btn = document.querySelector( "button.announce" );

btn.addEventListener("click", function( e ){
  this.ariaNotify( "Hello, world.", {
    priority: "high"
  });
});

The default priority for these notifications is priority: "normal", which behaves like aria-live="polite" (or role="status"). Setting an explicit priority: "high" will prioritize and potentially interrupt the current narration, the way aria-live="assertive" (or role="alert") would.

That’s the entire API so far, right there. No fussing with markup, no finessing timings; if you need something narrated, you call ariaNotify and it is narrated, just like that. You can try this out in Firefox as we speak, though it doesn’t seem like lang attributes are factored in just yet:

JAWS

Polite, button. To activate, press space bar. [spacebar pressed] Space. Hello, World.

Assertive, button. To activate, press space bar. [spacebar pressed] Space. Hello, World.

Educado [pronounced correctly], button. To activate, press space bar. Space. Hola, Mundo [pronounced incorrectly].

NVDA

Polite, button. [spacebar pressed] Hello, World.

Assertive, button. [spacebar pressed] Hello, World.

Educado [pronounced correctly], button. [spacebar pressed] Hola, Mundo [pronounced incorrectly].

VoiceOver

Polite, button. You are currently on a button [spacebar pressed] inside of a frame. To click this button, press ControlOptionSpace. To exit this web area, press ControlOptionShift-Up Arrow. Hello, World.

Assertive, button. You are currently on a button [spacebar pressed] Hello, World.

Educado [pronounced correctly], button. You are currently on a button [spacebar pressed] inside of a frame. To click this button, press ControlOptionSpace. To exit this web area, press ControlOptionShift-Up Arrow. Hola, Mundo [pronounced incorrectly].

Pretty solid, huh? Huge improvement over how we’ve all been stuck using live regions. I, for one, can’t wait to almost use ariaNotify, then — again — promptly talk myself out of it!

Why the reluctance? Well, in accessibility circles, it is sometimes said that there are three stages of learning to use ARIA:

  1. You don’t use ARIA.
  2. You use ARIA.
  3. You don’t use ARIA.

The W3C puts this in more formal terms, as is their specialty:

If you can use a native HTML element [HTML] or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.

Using ARIA: First Rule of ARIA Use

That second stage of ARIA mastery is where we get ourselves into trouble. The web is a chaotic place, but assistive technologies have evolved alongside it, and they’ve learned to paper over some of the more common issues a user might encounter. For example, say you have an h2 that reveals some visually-hidden content that follows it when clicked — that element might be presented in a way that makes that interaction clear visually, but without our intervention, it might not otherwise be signaled to a user browsing via screen reader. To work around this, assistive technologies can helpfully narrate that heading element as “clickable” when it receives user focus upon finding a click event listener bound to that heading. Granted, that isn’t as good as explicitly signaling the purpose of this element to the user, but it is workable, even if we didn’t make that interaction explicit.

The catch is in how we that make that interaction explicit. If you‘re somewhat familiar with ARIA, you might find yourself thinking “well, this element behaves like a button, so I should put role="button" on it to inform a user that this does something.” That impulse isn’t strictly wrong, but with that attribute comes a likely unintended consequence: by being explicit about the element’s role, we remove its implicit meaning. You’re telling the browser and assistive technology, in no uncertain terms, that this is not a heading — so if the user is navigating by way of the document outline, this element will no longer be part of that navigation, and what felt like a simple, helpful quick-fix ends up having an unintended consequence. ARIA leaves no room for interpretation; what we say goes, full stop. We say “narrate this,” it gets narrated. Non-negotiable.

So, given a very easy-to-use feature that inarguably says “when I tell you to narrate this, you narrate it,” please assume that I am making steely, unblinking eye contact here while I say, aloud, “alert()” — an imagined scenario made all the more unsettling by the fact that I have somehow managed to say it in a monospaced font.

window.alert( "Hello world, like it or not." );

You remember alert() from way back in the day, right? A method as infamous as it is obnoxious. If you’re newer to the industry, you might not be familiar with it first-hand, for a blessing. Like ariaNotify, it was — is, technically — a quick and easy API for immediately presenting information to a user:

A CSS-Tricks article with an alert on top that identifies the site address and says 'Hello World, like it or not.'

alert() is simple, effective, consistent, and — back when it saw widespread usage — incredibly annoying. ariaNotify() entrusts you with this same power, this time backed by the invisible, unyielding authority of ARIA. With ariaNotify() in your pocket, “this might be confusing, so I’ll narrate exactly what’s going on” will be a quick and easy decision, and might mean that a savvy user — already skilled at navigating the web despite all its inherent chaos and inconsistency — finds their browsing interrupted by a lecture about an interaction they already understand.

It isn’t hard to imagine a developer — their heart squarely in the right place — using ariaNotify to inform a user that content has been revealed by an interaction on the page. In context, however, revealing that content likely meant interacting with an element already narrated as “clickable” thanks to the presence of an associated event listener, the element’s inherent semantics, or the presence of an aria-expanded="false" attribute (the correct approach to signaling that interacting with an element will reveal associated content). In that case, all we’ve done is add noise to the user’s experience of the page, and nobody needs that. I mean, imagine being partway through reading this sentence when alert( "There's a new comment on this article!" ) interrupts you for the third time, or hovering over <button>Navigation</button> only to get hit with alert( "Click here to open the navigation." ) like some unskippable video game tutorial? Ugh. I’d close the tab.

Even worse, if a narrated instruction falls out of sync with the reality of the interaction itself — an invisible inconsistency that wouldn’t be caught by a QA process that lacks dedicated screen reader testing — we could end up making the user sit through an argument between the underlying page and their own screen reader while they’re just trying to get things done and get on with their day.

ARIA is powerful stuff. It gives us the ability to define the meanings, states, and relationships between elements on a page as absolute, iron-clad fact — it provides a line of communication between those of us building the web and those of us using it. Nowhere is that line of communication more direct than with ariaNotify(), a feature that effectively allows us to speak directly to an end user using the voice of the browser and assistive technology they know and trust. That’s a lot of responsibility bound up in a single method. It solves a very real problem, but like so many technologies: if not used carefully, it can cause just as many.

I am excited about ariaNotify(), y’know, in a measured, cautious way. It finally gives us a way to address a use case that has plagued the web — and me, personally — for years, in a shockingly easy way. So easy, in fact, that it makes ariaNotify() just a little bit dangerous.

I mean, not for us though, right? Because we’re all gonna be cool about this, right?

Right.


The Siren Song of  ariaNotify() originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Clear today!

With a high of F and a low of 59F. Currently, it's 75F and Clear outside. Current wind speeds: 11 from the Southeast Pollen: 3 S...