Skip to main content

Two examples of hover styles on images

I enjoy adding :hover styles to my websites. A good hover style reminds me of how fast and responsive our computers can be, if we let them. For example, I add a thicker underline when you hover over a link on this site, and it appears/disappears almost instantly as I move my cursor around. It feels snappy; it makes me smile.

I want to show you a pair of hover states I’ve been trying for images.

Adding an image border on hover

If I’m showing a small preview of an image that’s a clickable link to the full-sized version, I like to add a coloured border when you hover over it. It’s a visual clue that something will happen when you click, and “see the big version of the image” is a pretty normal thing to happen.

Initially I implemented this by adding a border property on hover, for example:

a:hover img {
  border: 10px solid red;  /* don't do this */
}

But a border takes up room on the page, which causes everything to get rearranged around it. Everything moves to make space for the border that just appeared, which is precisely what I don’t want. I want a subtle hover, not a disruptive one!

There are ways you can prevent this movement, but I couldn’t get them to work in a way I found satisfactory. For example, you could add a negative margin to offset the border, or an always-on transparent border that only changes colour when you hover – but those can interfere with other CSS rules. It became a game of whack-a-mole to make all my margins work in a consistent way.

The better approach I’ve found is to add a box-shadow with no blur – this looks like a border, but it’s purely visual and doesn’t take up any space on the page. This is the rule I use:

a:hover img {
  /* Four length values and a color */
  /* <offset-x> | <offset-y> | <blur-radius> | <spread-radius> | <color> */
  box-shadow: 0 0 0 10px red;
}

Here’s a demo of both approaches. Notice how the rest of the page moves around when you add a border, but not when you add a box-shadow:

border

box-shadow

(If you’re on a device that doesn’t support hovering, you can toggle the hover styles manually.)

Update, 23 October 2024: Several people wrote to me to tell me about CSS's outline property, which takes the same arguments as border but doesn’t take up any space.

I had a vague memory of trying this and it not working, but I couldn’t remember why.

I did some investigating and discovered what looks like a bug in Safari/Webkit. If you apply text-decoration styles on hover, they prevent any outline styles from appearing. Given that Safari is my primary browser and I use a lot of text-decoration styles, I imagine I tried outline at some point, ran into this issue or something similar, and I never thought about the property again.

(This isn’t the first hover-related bug I’ve encountered in Safari. I ran into another bug in March, and I’ve filed this outline/text-decoration bug as bug 282009.)

If you don’t use text-decoration or you don’t care about Safari/WebKit support, then you may find outline very handy. It uses the same syntax as border, so it’s easier to remember than box-shadow.

a:hover img {
outline: 10px solid red;
}

Changing the colour of icons on hover

A while ago I added social media links to the footer of this website. I displayed them as subtle, monochrome icons, to avoid overwhelming the footer with an explosion of different brand colours. I thought it would be fun to show the site’s brand colour when you hovered over the icon. If, say, you hover over the bird icon and see Twitter’s shade of blue, it’s a subtle confirmation that this is indeed a link to my Twitter profile.

Here are all the icons I had in this system:

(If you’re on a device that doesn’t support hovering, you can toggle the hover styles manually.)

The brand icons come from the websites themselves; the generic icons are from the Noun Project. When I downloaded the icon, I typically got an SVG file with one or more path elements that defined the icon’s shape.

For example, this is what I got in the SVG file for the email icon:

<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
 <path d="m323.46 411.07 …"/>
</svg>

To turn this into my footer icon, I wrapped the path in a slightly more complex SVG:

<svg width="30px" height="30px" version="1.1" viewBox="0 0 950 950" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <defs>

    <!-- Wrap the original envelope shape in a named group -->
    <g id="envelopeIcon">
      <svg x="-125" y="-125">
        <path d="m323.46 411.07 …"/>
      </svg>
    </g>

    <!-- Define a mask that blocks out the envelope shape -->
    <mask id="envelope">
      <rect x="0" y="0" width="950" height="950" fill="white"/>
      <use xlink:href="#envelopeIcon" fill="black"/>
    </mask>
  </defs>

  <!-- Define two shapes that actually get drawn -->
  <circle cx="475" cy="475" r="450" class="background"/>
  <circle cx="475" cy="475" r="475" class="foreground" mask="url(#envelope)"/>
</svg>

First I define a shape envelopeIcon which uses that original path, and defines the shape of the element as a reusable group. Then I define an SVG mask envelope that blocks out the envelope shape. Finally, I define two shapes that actually get drawn: a foreground and a background.

Here’s a 3D view of the two shapes, so you can see more clearly how they form a set of layers:

foreground background

Now I have two elements that I can style independently. For example:

.email .foreground { fill: gray; }
.email .background { fill: none; }

a:hover .email .foreground { fill: blue;  }
a:hover .email .background { fill: white; }

This technique can be extended to more complex icons – split it into multiple elements, and style each one independently.

It took me a while to get these icons working as I wanted, and I remember trying some quite fiddly hacks. As I was writing this post, I was pleasantly surprised to discover that the hover styles aren’t as complicated as I thought. I just had to figure out the general approach.

One thing I’m proud of is how readable this site is without CSS. I made these icons look good on a sans-CSS page by adding inline fill attributes to the background/foreground elements (e.g. <circle fill="white" …>). These inline styles get applied even if CSS is broken or disabled. I lose the interactivity, but the icon is still legible.

By the time you read this, those icons will have vanished from the footer. They were fun for me to work on, but almost nobody used them and they added almost 8KB of HTML to every page. That might not seem like much, until I tell you the average page size was a slender 21.1KB – nearly 40% of my HTML was spent on social media links nobody was clicking!