Cool WP Tavern Image Hover

I was catching up on my WordPress news this morning over at WP Tavern. Normally, that’s something I’d do in my RSS reader (because RSS is boss), but I hopped over to the WP Tavern site instead and spent more time admiring its image hover effect.

????

I love, love, love little effects like this. It ain’t the fanciest or most complex thing in the world, but that’s the appeal. The interaction feels so natural that — like a good drum beat — you might not even notice. It’s also the inverse of those image hovers that zoom into the image instead. That CodePen demo I just linked to… that’s from 2016. We’re not charting new territory or anything.

But CSS has changed a bunch since then. I already had an idea of how I would tackle this myself, but I wondered how I might do it in the context of newer CSS features.

The Markup

Normally, I’d start with some markup. That doesn’t change in these newfangled times.

<a href="#"> 
  <figure>
    <img src="/path/to/image.avif" alt="">
  </figure>
</a>

The markup is incredibly important in terms of determining the approach. This is totally how I may have written the HTML for something like an image hover effect that’s linked to another page. It’s wrought with accessibility issues and might just be a bad idea. But nesting everything inside the link cuts us some slack because we can style the <figure> container based on the hover or focus state.

a:hover figure {
  /* style figure on link hover */
}

I suppose we could ditch the <figure> altogether and simply use the link as the container, but I like the semantics of having it there.

These days, we can do it differently:

<figure>
  <a href="#"> 
    <img src="/path/to/image.avif" alt="">
  </a>
</figure>

Mmm, much better. But how can we get the same styling affordances we get from link interactions if the link is now nested in the container?

Hello, :has()

Commonly called the “parent” selector,” :has() can select an element based on what is inside it. It’s much more than a “parent selector,” but that’s a good way to describe what we’re able to do with it in this instance.

figure:has(a) {
  /* style the figure if it contains a link child */
}

That’s probably a good place to start! Instead of simply selecting <figure>, we can target just the instances when a link is a direct child of the figure. This way, the CSS is a little more defensive as far as impacting other figures that are styled differently when they do not contain links.

OK, so if a figure contains a link, we’re going to add a bottom border to it as WP Tavern does.

figure:has(a) {
  border-bottom: 2px solid #D33939;
  margin: 0;
  padding-bottom: 1.5rem;
}

Nothing complicated, right? We can make it better with some newer CSS goodies, though. Let’s try this instead:

figure:has(a) {
  /* Using CSS variables for repeated values */
  --color: #D33939;
  --padding: 1.5rem;
  
  /* Using logical properties */
  border-block-end: 2px solid var(--color);
  margin: 0;
  padding-block-end: var(--padding);
}

CSS variables ain’t all that new, but they are certainly new since 2016. And logical properties are what I’m defaulting to these days. For one thing, I teach beginning-level front-end development, and I think logical properties help students understand the difference between block and inline flows which is the foundation for building layouts. Secondly, logical properties are another good defensive measure, prepping a layout for different writing modes in a multilingual context. It may not be totally necessary, but it makes for a more robust set of styles.

CSS Nesting

I want that image to take up the full width of the figure, so we should account for that while we’re at it:

figure img {
  width: 100%;
}

Cool, but modern CSS gives us a way to make it more obvious that the image is connected to the figure than using a compound selector — CSS nesting! Now that image can be nested in the figure’s ruleset like we would be able to do in Sass or whatever CSS preprocessor you fancy.

figure:has(a) {
  --color: #D33939;
  --padding: 1.5rem;

  border-block-end: 2px solid var(--color);
  margin: 0;
  padding-block-end: var(--padding);

  & img { width: min(100%);  }
}

Two rulesets in one! This is something I’ve wanted in CSS forever, and it blows my mind that we finally have it here in 2023. Rock ‘n’ roll!

(I changed up the width value a bit there using the min() function. Totally unnecessary as it adds extra code, but I figure this is more of a challenge to use modern CSS features. So, when in Rome…)

Transitions

I want two things to happen on hover:

  • The image shrinks a bit
  • The figure gets a full border

I may have approached it like this once upon a time:

figure a {
  display: block;
  padding-block-end: (--padding);
  transition: all .25s ease-out;
}

figure a:hover {
  border: 2px solid var(--color);
  padding: var(--padding);
}

figure a:hover img {
  transform: scale(.8);
}

That’s a lot of CSS for a little effect like this. Might as well put :has() and nesting back to work to make this cleaner:

figure:has(a:hover) {
  border: 2px solid var(--color);
  padding: var(--padding);
}

Huh, that’s it?!?! It sure is! When the figure has a link that is in a hovered state, we draw a full border around it and add uniform padding on all the edges. We don’t even need to rescale the image because the padding will do it for us.

Soooooooo coooooooool.

✏️ Handwritten by Geoff Graham on June 16, 2023

2 Comments

  1. # June 18, 2023

    I really love :has, but I don’t feel comfortable relying on it until it works in Firefox without enabling a flag. I suppose it’s fine if what you’re doing is progressive enhancement, but it’s still always worth asking yourself whether it’s worth it each time!

    I implemented an effect a bit like this a few days ago for the boxes on the new homepage of DanQ.me and after some flip-flopping eventually went with a “classic” class-based approach to differentiate whether a particular link “:has” a a certain child element. If I didn’t mind the larger payload I suppose I could’ve implemented it both ways (perhaps with a @supports selector) which’d make swapping it out later easier too… hmm, maybe I’ll do that!

    Reply

Leave a Reply

Markdown supported