> All in One 586

Ads

Friday, June 12, 2026

There’s no need to include ‘navigation’ in your navigation labels

Mark Underhill:

And now to the reason I wrote this post: including the word “navigation” in your <nav> labels. There’s no need. If we did, we’d hear something like “Navigation, Primary navigation”. Not the end of the world, but unnecessarily repetitive for screen reader users.

One of those nuances to keep in your back pocket when writing for screen readers. Reminds me, too, that there’s no need to say something like “image” when describing one in the alt text. That’s sorta implied. While I’m no screen reading native, I imagine these sorts of things are minor pet peeves that, given a little love and consideration, make navigating that much more enjoyable.

While we’re on the UX of accessible text, another consideration: keep it succinct. It doesn’t have to be a novel.


There’s no need to include ‘navigation’ in your navigation labels originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Why Isn’t My 3D View Transition Working?

If you have played around with view transition a bunch, you may have noticed that 3D transitions between two pages (i.e., cross-document view transitions) don’t seem to work. That is, at least not without the browsers flattening things first.

Image elements are the best example to demonstrate this because, like the snapshots a browser takes of the before-after states in a view transition, images are replaced elements so, in theory, we should be able to use them as a sort of reduced test case for 3D animations. For example, flipping one image to reveal another on click looks like this:

It’s important to note that, for the animation to work properly, we need to set the perspective property on the image’s parent container (in our case, it’s the .scene element). Otherwise, the 3D transformation is merely flat. It sort of angles the element’s appearance:

In CSS, the parent’s persepective is applied to all its children, excluding itself:

.scene {
  perspective: 1200px;

  .card { /* gets perspective */ }
}

What’s important here is the HTML structure. Specifically how the .scene container sits on top of the child .card elements, making the 3D effect come to life so the flip looks how it should:

<div class="scene">
  <div class="card">
    <!-- Card Content Here -->
  </div>
</div>

Perhaps our keyframe animation to flip the .cards is something like this:

@keyframes flipOut {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(-90deg);
  }
}

Which we apply to the .cards like this:

.card.flip-out {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.card.flip-in {
  animation: flipOut 5.2s cubic-bezier(0.4, 0, 0.2, 1) forwards reverse;
}

…where the animates runs forwards when the .flip-out class is appended to the .card (courtesy of JavaScript watching for a click) and runs in reverse when the .flip-in class is appended.

That’s the setup for how a cross-document view transition ought to work, too, right? If an image supports a 3D animation, then a view transitions snapshot should do the same. Let’s poke at that.

Setting up the view transition

First things first, we have to opt into view transitions on both pages with the @view-transition at-rule by setting the navigation descriptor to auto:

@view-transition {
  navigation: auto;
}

If we were to do nothing else, then one page fades into another when navigating between the two. It’s the most basic of all cross-document view transitions.

How do we customize things? We use the ::view-transition-old() and ::view-transition-new() pseudo-classes, where the former is the “old” snapshot and the latter is the “new” one. Like the .card elements we used in the last example, that’s where we set the keyframe animation:

::view-transition-old(root) {
  /* animation goes here */
}

::view-transition-new(root) {
  /* animation goes here */
}

The root parameter tells the view transition to target the whole page and all the elements created (and not created) by the view transition’s default snapshot group.

Here’s the problem

Let’s say we want to apply that same 3D flip to the entire webpage, where the snapshot of the “old” page flips into the “new” page. Again, a 3D animation asks us for two things:

  1. The perspective property on the parent element so its children get that 3D effect
  2. An animation on the page for when the view transition happens

But: What exactly do we set the perspective on, as in, what is the parent element here?

Since view transitions take snapshots of the entire webpage, we might assume (logically) it would be the <html> element (or the :root), right? I mean, the DOM tree looks like this when a view transition is present:

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  └─ ::view-transition-group(name)
  │     └─ ::view-transition-image-pair(name)
  │        ├─ ::view-transition-old(name)
  │        └─ ::view-transition-new(name)
  ├─ head
  └─ body
        └─ …

So, the entire snapshot should be where we put the perspective. Right? Turns out, no.

In fact, does nothing at all! You’re left with this instead of the beautiful 3D flip we were able to use on the cards earlier:

GitHub Source and Live Demo

Here’s the code I was working with:

/* Cross-document View Transition opt-in */
@view-transition {
  navigation: auto;
}

/* 3D flip: Old page flips away, new page flips in */
@keyframes flip-out {
  0% {
    transform: rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: rotateY(-90deg);
    opacity: 0;
  }
}

@keyframes flip-in {
  0% {
    transform: rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: rotateY(0deg);
    opacity: 1;
  }
}

::view-transition-old(root) {
  animation: flip-out 0.3s cubic-bezier(0.4, 0, 1, 1) forwards;
  transform-origin: center center;
}

::view-transition-new(root) {
  animation: flip-in 0.3s cubic-bezier(0, 0, 0.6, 1) 0.3s backwards;
  transform-origin: center center;
}

Note: I didn’t reverse the animation here since we flip to -90deg and then from 90deg. Not exactly the same!

And it doesn’t work, no matter if perspective is on html or :root:

/* 👎 */
html {
  perspective: 1100px;
}

/* 👎 */
:root {
  perspective: 1100px;
}

I did some digging and discovered that perspective (and 3D transformations in general) is one of several CSS properties that would produce an unusual effect. (Leave it to Bramus to have the answer!)

So… What do we do? Some ideas came to mind, but sadly failed:

  • I tried setting the perspective property on the body.
  • I tried setting perspective inside ::view-transition-group(root).
  • I tried setting perspective inside the ::view-transition pseudo.

There’s actually a super simple workaround to this, and I can’t believe it took me this long to figure it out — don’t use perspective at all!

The solution

Short story: we have to use the perspective() function instead of the perspective property. And not inside any of the ::view-transition-* pseudos as you might expect, but inside the @keyframes animation:

@keyframes flip-out {
  0% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: perspective(1100px) rotateY(-90deg);
    opacity: 0;
  }
}
@keyframes flip-in {
  0% {
    transform: perspective(1100px) rotateY(90deg);
    opacity: 0;
  }
  100% {
    transform: perspective(1100px) rotateY(0deg);
    opacity: 1;
  }
}

This simple, but big change moves the scene from a flat meh to a beautiful ah yeah:

GitHub Source and Live Demo

Here’s why, apparently. The view transition pseudo-element tree is rendered outside the normal HTML flow. More specifically, the entire view transition tree is rendered above the DOM in its own layer. However, particularly for ::view-transition, I’m not too sure why this is the case, but my best guess would be that each view transition group automatically has its position and transform values overridden by the browser; hence, interfering with the perspective.

The difference between perspective and perspective()? The perspective property is applied to the parent element, while perspective() is a transform property function applied directly to the element itself. And since the view transition pseudo tree does not have a true parent, we’ve gotta use perspective() since it doesn’t require a parent. Phew.

To recap…

Setting perspective on the html:root, or any of the view transition pseudo-class won’t work. And if you have been struggling to find the solution, like I was, I think this little, but big perspective() change will solve that issue if you ever come across it. Take it from me, I battled with this for weeks till I came back today to rant about it and discovered a solution to it. A perk of writing!


Why Isn’t My 3D View Transition Working? originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Thursday, June 11, 2026

Clear today!



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

Current wind speeds: 7 from the East

Pollen: 3

Sunrise: June 11, 2026 at 05:24PM

Sunset: June 12, 2026 at 08:18AM

UV index: 0

Humidity: 34%

via https://ift.tt/uHjDpGo

June 12, 2026 at 10:02AM

Wednesday, June 10, 2026

Clear/Wind today!



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

Current wind speeds: 10 from the East

Pollen: 3

Sunrise: June 10, 2026 at 05:24PM

Sunset: June 11, 2026 at 08:17AM

UV index: 0

Humidity: 31%

via https://ift.tt/xag6FOi

June 11, 2026 at 10:02AM

Creating Memorable Web Experiences: A Modern CSS Toolkit

I love the fact that CSS is finally reclaiming control over visual interactions, taking charge of the styling, the animation, and the accessibility exactly as it should. Today, native browser capabilities allow us to move the heavy lifting away from the JavaScript main thread and closer to the GPU. By letting the browser’s engine optimize performance under the hood, we save energy and processing power while building code that is robust, accessible, and independent of external libraries that might deprecate tomorrow.

We have 3D, modern layout techniques, clip-paths, transforms, custom properties, scroll-driven animations, view-transitions, @property — and we can animate almost anything, even to auto-height!

And, of course, there’s SVG, which isn’t new, but allows us to build entire websites through illustrations and animations. Take the example below: it’s responsive, lightweight, accessible, and powered primarily by CSS Grid + SVG.

We can even build an entire video game including the UI using only SVG:

What follows is not a complete guide to modern CSS, but an opinionated selection of techniques I reach for when I want a site to feel alive and be remembered. There are many ways to create memorable experiences. Sometimes it’s as simple as a form that completes smoothly. But here I’m interested in the expressive end of the spectrum.

Motion as Communication: Defining Your Intent

Before we dive into the technical side, I want to clarify something: we shouldn’t move things just because we can.

Everything communicates, and our animations are no exception. We must take the time to design movements that support the message we want to convey in order to keep our intents tightly scoped without overdoing it.

Here’s a methodology I use when planning the design and animation of a site.

Imagine we’re working on a project for a nature event focused on mushrooms. The design language changes completely depending on the “vibe”: selling a “Psychedelic Mushroom Rave” is worlds apart from a “Spiritual Mushroom Retreat” focused on ancestral medicine.

Every design decision communicates. I like to create what I call keyword lists to define my intent and scope. For example, I might break things down into different options:

Option A: The Psychedelic Event

  • Visuals: Colorful, saturated, high-contrast, illustrations, distortions
  • Movement: Fast, frantic, unpredictable, morphing, rhythmic, synced loops, hypnotic
  • Feeling: Fun, chaotic, energetic, stimulating, surprising
  • Typography: Funk, “psych-rock”
  • Style References: Pop Art, 60s/70s op art, rave flyers
  • Actions: Dancing
  • Extras: Emojis, films (e.g., Fear and Loathing in Las Vegas)

Option B: The Spiritual Retreat

  • Visuals: Earth tones, neutral tones, de-saturated, photograph-heavy, nature, whitespace
  • Movement: Slow, fluid, organic, breathing, subtle parallax, smooth scrolling.
  • Feeling: Calm, serene, introspective, contemplative, safe
  • Typography: Elegant Serif, minimalist sans-serif, wide spacing, legible
  • Style References: Scandinavian design, Japanese Wabi-sabi, wellness/spa aesthetics, botanical books
  • Actions: Breathing
  • Extras: Healing sounds, film (e.g., Eat Pray Love)

This is the kind of exercise I do to guide my design and animation decisions. The lists will help me select everything from which CSS properties I plan to use and how to use them. I even share them with the client and, together, we choose a direction.

Let’s say we go with Option A and look at a few examples of what I think are essential ingredients for creating memorable user experiences.

Split Text Animations

These animations became popular thanks to the GSAP SplitText plugin. It splits text by character (or words, or lines if you like) so we can create interesting text effects, like staggered animations.

<h1 class="reveal-text">
  <span style="--i:0">H</span>
  <span style="--i:1">O</span>
  <span style="--i:2">L</span>
  <span style="--i:3">A</span>
</h1>

This approach wraps each letter in “Hola” in a span. From there, each span is inline-styled with a custom property indexing the spans in order. Which is something that will get a lot easier when the sibling-index() function gains broad browser support.

But for now, each custom property value acts as a multiplier that increases an animation-delay, staggering each span. In this case we fade in each character as it moves up.

.reveal-text span {
  animation: slideUp 0.6s ease-out forwards;
  animation-delay: calc(var(--i) * 0.1s);
  display: inline-block;
  opacity: 0;
  transform: translateY(3rem);
}

@keyframes slideUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Accessibility is the tricky part here. The instinct is to hide all the individual spans from assistive technology with aria-hidden="true" and add a visually hidden version of the full word for screen readers:

<h1>
  <span class="sr-only">HOLA</span>
  <span aria-hidden="true" class="reveal-text">
    <span style="--i:0">H</span>
    <span style="--i:1">O</span>
    <span style="--i:2">L</span>
    <span style="--i:3">A</span>
  </span>
</h1>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

But be warned: this pattern doesn’t guarantee a good experience across all screen readers. Adrian Roselli tested GSAP’s SplitText across eight screen reader and browser combinations and found it only worked correctly in two of them. If you ship this technique, test it with real assistive technology.

If that risk feels too high, there’s a very clever alternative from Preethi worth knowing that uses the letter-spacing property. It accepts negative values that collapse characters on top of each other, hiding them without touching the DOM at all. Animate it back to 0 and you get a similar reveal effect without accessibility overhead.

What would be great is a pseudo-selector like ::nth-letter to target individual glyphs directly from CSS the way ::first-letter selects the first character. But unfortunately, there’s no ::nth-letter… at least yet.

Remember to respect the user’s motion preferences on every animation:

@media (prefers-reduced-motion: reduce) {
  .reveal-text span {
    animation: none; /* or a softer animation */
  }
}

And here we go:

It might not scale too much when we have a lot of text and different animations we want to apply. For the psychedelic event, I wanted to try splitting text with SMIL, but it was verbose. This is the code for animating two letters alone:

<svg role="img" aria-label="TODOS LOS HONGOS" viewBox="0 0 1366 938.96">
  <title>TODOS LOS HONGOS</title>
  <g aria-hidden="true">
    <text transform="rotate(-9.87 2181.107 -1635.1)" opacity="0">T
      <animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0s" fill="freeze"/>
      <animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0s" fill="freeze"/>
    </text>
    <text transform="rotate(-8.92 2372.854 -2084.755)" opacity="0">O
      <animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0.1s" fill="freeze"/>
      <animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0.1s" fill="freeze"/>
    </text>
    <!-- rest of letters... -->
  </g>
</svg>

Add role="img" and a <title> to the <svg>, and wrap the individual letters in <g aria-hidden="true">. That gives screen readers one clean label to read. It works well in some combinations and badly in others, so if the text is critical, don’t animate it.

Here is the complete code. It’s easier to write it when you have an AI to do it for you:

For longer text, a library like GSAP gives you more control, but the same accessibility risks we discussed earlier apply, and the results across screen readers are inconsistent:

<h1>
  <span class="splitfirst">Todos los hongos son</span>
  <span class="splitlast">mágicos</span>
</h1>
const splitFirst = SplitText.create('.splitfirst', {
  type: "chars",
});
const splitLast = SplitText.create('.splitlast', {
  type: "chars, lines",
  mask: "lines"
});

const tween = gsap.timeline()
.from(splitFirst.chars, {
  xPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1, 
})
.from(splitLast.chars, {
  yPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1,
});

This would be a nice approach for Option B if we had gone that route. See how “serene” things feel as the text fades in.

Masking & Clipping

The clip-path and mask properties allow us to hide portions of an element, but they work on fundamentally different principles. Clipping is a binary decision: pixels are either fully visible or completely gone,  making it the right choice for clean geometric shapes, like polygons, circles, or SVG paths, where the browser can also optimize rendering more efficiently. Masking, on the other hand, uses luminance or alpha channel values: white reveals, black hides, and everything in between produces partial transparency. This makes it the tool for soft edges, gradient fades, and irregular textures. Keep in mind that if you have a very complex vector shape, it might be more performant to use a mask than a vector clip-path. Sarah Drasner has a nice write-up on when it makes sense to use one over the other.

Our project is a very clear use case for clip-path. We have a circle shape that starts with clip-path: circle(0%), which makes the element invisible (the clipping circle has zero radius). Over the duration of the animation it expands to circle(100%), which fully reveals the element as the circle grows outward from its center. Meanwhile, we fade things in with the help of opacity.

#rainbow, #floor, #mushroom, #flores {
  opacity: 0;
  animation: maskAnim 2s ease-in forwards;
}

@keyframes maskAnim {
  0%, 1% { 
    clip-path: circle(0%);
    opacity: 1; 
  }
  100% { 
    clip-path: circle(100%); 
    opacity: 1; 
  }
}

Note: The 1% keyframe is there to make sure the browser starts the clip-path interpolation from circle(0%) rather than from whatever value the element might already have. Without it, some browsers will unexpectedly jump at the very start. A cleaner alternative is to use animation-fill-mode: both because it locks the element in its from state before the animation begins.

From there, we apply the same animation to the different SVG groups in our illustration:

<g id="rainbow">...</g>
<g id="floor">...</g>
<g id="mushroom">...</g>
<g id="flowers">...</g>

How psychedelic is this?!

Scroll-Driven Animations

Scroll-driven animations are great because we can connect an animation’s progress to the user’s scrolling instead of a typical timeline that runs and stops.

We can use it for subtle and somewhat “trippy” movement, like a light parallax effect. In this case, we can make things that appear closer to the user move faster than the ones that are more distant.

This is the full CSS:

#estrellas, #arcoiris, .text-line, #fecha, #arco, #flores, #dir, #piso, #barras {
  animation: moveUp both;
  animation-timeline: view();
}

@keyframes moveUp {
  from { transform: translateY(var(--offset)); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

#estrellas { --offset: 10vh; }
#arcoiris { --offset: 20vh; }
#fecha { --offset: 45vh; }
#arco { --offset: 50vh; }
#dir { --offset: 50vh; }
#flores { --offset: 65vh; }
#piso { --offset: 85vh; }
#barras { --offset: 90vh; }

The animation-timeline: view() says that things should start the animation as soon as an element enters the scrollport when the user scrolls into it, and fully completes when it scrolls out of view. To make things move at different velocities, we place them at different offsets using an indexed --offset custom property like we did earlier for splitting text.

3D Transforms

This one is trickier and we need to keep an eye on performance. A tool like Layoutit can help carry the lift because it has a voxels and terrain generator built entirely with CSS 3D. It can go even further when it’s complemented with VoxCSS, a full voxel engine that renders 3D cuboids using only CSS Grid layers and transforms without the complexity of Canvas or WebGL.

Let’s put together some combination scrolling and 3D effects. It’s the sort of thing that supports the “hypnotic” and “dancing” ideas in the Option A keyword list. Check this out:

Here, I’ve set up a scene with depth using the perspective property and then wrap all the child elements inside the scene in a 3D space with transform-style: preserve-3d. This way, all the child image elements rotate and translate along the depth axis (or z-axis).

Let’s connect that to a scroll-driven animation that uses transform: rotateY:

.scene {
  perspective: 1200px;
}

.img-wrapper { 
  transform-style: preserve-3d; 
  animation: rotateImg linear;
  animation-timeline: scroll();

  > img {
    transform: rotateY(270deg) translate3d(0, 50px, var(--distance));
  }

  > img:nth-child(2) {
    transform: rotateY(180deg) translate3d(0, 50px, var(--distance));
  }
}

/* etc. */

@keyframes rotateImg {
  to { transform: rotateY(360deg); }
}

Custom Cursors

cursor might be one of the most unused CSS properties. There are many cursor types we can use, although there are definitely opinions on just how far to go with this.

And we can use it to play around with the images, displaying different cursors on different containers when the user hovers them. I would personally use an SVG and PNG image for transparency support, though the property supports any raster image.

It’s worth noting that cursor sizes vary by browser: Firefox caps custom cursors at 32×32px, while Chrome supports up to 128×128px. Most browsers refuse to display — or will downscale — cursors that are larger than 32×32px on high-DPI (retina) screens. Keeping your cursor at 32×32px is the safest choice to ensure consistency.

For example:

.box1 {
  cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAZCAMAAAD63NUrAAAACVBMVEX///8AAAD///9+749PAAAAAXRSTlMAQObYZgAAAFZJREFUeNqdzksKwDAIAFHH+x+6lIYOVPOhs5OHJnES/5UkYKEkU7xjijSIm50iFh4fAXgYDd/yumVVRSwsqq/nRA3xVK0oo06d5U6DpQZ7PV7lMxH7LkaQAbYFwryzAAAAAElFTkSuQmCC),auto; 
}

We can even set multiple fallbacks to ensure the widest level of browser support:

body {
  cursor: url('path-to-image.png'), url('path-to-image-2.svg'), url('path-to-image-3.jpeg'), auto;
}

While this is cool and all, we have to keep accessibility in mind for something that changes default web behavior like this. Custom cursors could be fun to apply to very specific elements rather than wholesale across the board.

Bonus: Anchor Positioning

One more thing before we wrap up. I’ve been playing with CSS Anchor Positioning, inspired by a Kevin Powell demo. We can use it to attach a single pseudo-element to a currently-hovered item instead of attaching a pseudo-element for each and every item. In other words, we create a single element and anchor it to a hovered element, like highlighting cards:

That opens up interesting possibilities, like being able to transition the hover state between cards. In this case, I’m using the linear() function to get that natural bounce with help from Easing Wizard.

Conclusion

The technical barriers for creating memorable web experiences are mostly gone now. I hope everything we’ve covered here gives you an idea of just how far we can go with modern CSS features that completely remove the need for additional JavaScript. We have more possibilities than ever before, all without the need for complex technical overhead like days past.

So, instead of asking, is this possible?, the most important question becomes, does this movement tell a better story? If yes, ship it. Use these tools not because you can, but because they help you tell a better story, one that is also accessible and performant.

And, of course, everything in here is just a handful of ways to do that. But what sort of memorable experiences have you used in your work? Or what have you seen on other sites?


Creating Memorable Web Experiences: A Modern CSS Toolkit originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.



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

Tuesday, June 9, 2026

Partly Cloudy today!



With a high of F and a low of 56F. Currently, it's 74F and Clear outside.

Current wind speeds: 16 from the South

Pollen: 3

Sunrise: June 9, 2026 at 05:25PM

Sunset: June 10, 2026 at 08:17AM

UV index: 0

Humidity: 39%

via https://ift.tt/WTesxd5

June 10, 2026 at 10:02AM

Monday, June 8, 2026

Partly Cloudy today!



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

Current wind speeds: 15 from the Northeast

Pollen: 3

Sunrise: June 8, 2026 at 05:25PM

Sunset: June 9, 2026 at 08:16AM

UV index: 0

Humidity: 85%

via https://ift.tt/vFlHANE

June 9, 2026 at 10:02AM

There’s no need to include ‘navigation’ in your navigation labels

Mark Underhill : And now to the reason I wrote this post: including the word “navigation” in your  <nav>  labels. There’s no nee...