Glowing hover effect with CSS

Posted by Charanjit Chana on 2024-01-15.

All inspired by a tweet from @aman684__, I wanted to look at building a hover effect that mimicked a pixelated interface that would glow as you dragged the mouse across it. Just without JavaScript and only driven by CSS. It felt like :has() would make it a breeze, but it turned out to be a bit tougher than expected.

My first attempt was with a table layout. It worked as a small demo but when I tried to build it out I turned to programatically building a long list of selectors that then caused my browser to lock up. It was an unexpected outcome.

I stopped and took some time to think and decided to go for a div based layout so that I wasn't having to work with rows and columns as separate concepts from the cells themselves. I basically ended up being able to always select n number of elements before and/or after the one being hovered. There's still a quirk where hovering too close to the left or right causes the glowing effect to appear on the opposite side. With a bit of effort, I reckon that could be overcome but I think the demo I have is a good place to pause.

The CSS explained

@function selectorBuilder($count) {
  $selector: '';
  @for $i from 1 through $count {
    $selector: $selector + "+ div ";
  }
  @return $selector;
}

A function that takes the number of items to iterate through and then append + div that many times.

.container {
  display: grid;
  grid-template-columns: repeat(20, 1fr);
  margin: 0 auto;
  width: min(100vw, 1000px);
}

.container div {
  --r: 255;
  --g: 255;
  --b: 255;
  --opacity: 0.1;
  aspect-ratio: 1;
  background-color: rgba(var(--r), var(--g), var(--b), var(--opacity));
  border: 1px solid #222;
  grid-column: span 1;
  width: 100%;
}

Create a square grid layout, using variables for building the various color values which then makes it easier to override different bits later on, depending on their visual vicinity to the square being hovered on.

.container div {
  &:has(+ div:hover),
  &:has(#{selectorBuilder(1)} + div:hover),
  &:has(#{selectorBuilder(17)} + div:hover),
  &:has(#{selectorBuilder(18)} + div:hover),
  &:has(#{selectorBuilder(19)} + div:hover),
  &:has(#{selectorBuilder(20)} + div:hover),
  &:has(#{selectorBuilder(21)} + div:hover),
  &:has(#{selectorBuilder(38)} + div:hover),
  &:has(#{selectorBuilder(39)} + div:hover),
  &:has(#{selectorBuilder(40)} + div:hover) {
    --opacity: 0.5;
  }
}

There are lots of variations of the above where I am building out the selectors. In this case, the squares that looking like they're immediately around the item being hovered on are given an opacity of 0.5.

.container div {
  &:has(#{selectorBuilder(2)} + div:hover) {
    /* properties */
  }
}

As an example, the above generates this selector:

.container div:has(+ div + div + div:hover) {
  /* properties */
}
If the div is followed by 3 divs, and the third one is being hovered, then the div (3 before the one being hovered) will be caught by the selector.

Given my first attempt used up a lot of resources, I don't think this is a production-ready solution. That's before you factor in the quirks of how it behaves at the edges. But it's something that wouldn't have been possible without :has(). I'd long wanted a parent selector, but :has() just opens up a whole world of possibilities that I hadn't really thought about before..