Emphasizing & De-Emphasizing Link Interactions in Navigation

I hopped over to The Athletic hoping for some inkling of news that the San Francisco Giants had made some big splash of a trade or free agent signing. And just like the 99 other times I’ve checked in the last hour, there’s nothing shaking.

Anyway, I scouted this slick little interaction in the site’s navigation.

This isn’t some new invention or anything. I’ve seen it in the wild before but I guess needed a slow Giants offseason to pay it any mind. Looks like JavaScript is driving it in some way, perhaps watching for hover events and triggering a style transition. It’s just too “smooth” to convince me that CSS is handling it alone.

But it can! The idea is we want navigation links at some default level of color contrast. On hover, two different actions are triggered which results in the hovered link gaining more color contrast while the rest of the links lose additional contrast. It all adds up to something neat where we’re both emphasizing the hovered link and de-emphasizing the others in a single interaction.

Pretty cool.

So, I scribbled some unspectacular HTML for a <nav> element with a <ul> containing the links. I like this approach because we get the added bonus of screenreaders announcing the number of links since they’re contained in a list.

    <li><a href="#">
      <img src="https://assets.codepen.io/9632/mlb_1.png" alt="Major League Baseball" />
    <li><a href="#">Teams</a></li>
    <li><a href="#">Scores</a></li>
    <li><a href="#">Schedule</a></li>
    <li><a href="#">Standings</a></li>
    <li><a href="#">Podcast</a></li>

Of course, we don’t want those unsightly bullet markers that come with unordered lists, so we remove it in the CSS — only we do it using a clever trick that removes them without muffing up semantics. That, and a few baseline styles to get us going:

body {
  font-family: 'Sofia Sans Condensed', sans-serif;
  font-size: 18px;
  line-height: .75;
  margin: 0;
  padding: 0;

nav {
  background: hsl(215deg 100% 10%);
  overflow-x: scroll;
  padding: 0 2rem;

nav ul {
  align-items: center;
  display: flex;
  gap: 1.35rem;
  list-style: "";
  padding: 0;

I’m not sure anything in there is poignant and worth picking apart, only to say that I put scrolling on the <nav> in the horizontal direction simply as a cheap way to optimize things for smaller screens. Otherwise, we’re dressing up the unordered list as a flexible container that flows in the default row direction with a bit of a gap between list items.

Maybe it’s worth mentioning that I decided to keep the default underline decoration for links. I have a tough time removing them knowing that the WCAG indicates links should rely on more than just a change in color for “action, prompting a response, or distinguishing a visual element.”

OK, OK. Back to business. I figured this was a good excuse to work with CSS relational functions, like :has, :is, and :not. I took it more as a challenge to make sure I use them, so much so that I’m sure my approach is way heavy-handed in that regard.

I started by setting the links in white:

nav ul a:not(:has(img)) {
  color: hsl(0deg 0% 100%);

That selector looks overly obtuse at first glance, but it’s only because I also decided to set a brightness filter on the links while skipping the MLB logo that’s linked up as well. The super-duper official English translation of that selector, not available in Rosetta Stone, is:

Take a look at the links in the <nav> element contained in an unordered list. What I want you to do is select only the links that do not contain an <img> element.

Google Translate (maybe)

That’s how we’re able to decrease the brightness of the links while leaving the MLB logo in tact:

nav ul a:not(:has(img)) {
  color: hsl(0deg 0% 100%);
  filter: brightness(80%);

The rest of this is handling what happens to (1) the hovered link and (2) the non-hovered links. For that first one, what we’re really saying is, “If a link in the <nav> element is either hovered or in focus, might it brighter!”

Or, in code:

nav ul a:is(:hover, :focus) {
  filter: brightness(100%);

All that’s left is what to do with the rest of the links. This is where I really had to get out my English-to-CSS dictionary because what we want to say is something along the lines of, “If the <ul> element :has a link that is in a :hover or :focus state, I want you to select all of the links that are :not in a :hover or :focus state.

/* ???? */
nav ul:has(a:hover, a:focus) a:not(:hover, :focus) {
  filter: brightness(60%);
  transition: all .25s ease-in;

Now it’s like we’re having our emphasized link cake and eating our de-emphasizing link cake, too. I think what strikes me about pseudo-selectors like :has and :not is how we can make conditional selections based on the existence of something — even a state — and apply styles anywhere else in certain situations. Before you know it, we’ll have fully-reactive UI implementations purely in CSS! You know someone has already done experiments with UI changes by applying styles based on the existence of something in the body.

body:has(.whatever) {
  /* if this exists, apply these styles */
body:not(has(.whatever)) {
  /* if this does not exist, apply these styles */

Even cooler with web components?

body:has(custom-element) {
  /* if this exists, apply these styles */
body:not(has(custom-element)) {
  /* if this does not exist, apply these styles */
✏️ Handwritten by Geoff Graham on December 14, 2023

Leave a Reply

Markdown supported