73

I have an anchor that changes its background image when hovered with a class class-btn that contains a background-image.

When hovered, it has

a.class-btn:hover
{
    background-image('path/to/image-hovered.jpg');
}

When the page loads the first time and you hover this button the first time, it blinks (it takes about half a second to download the hovered image). How to avoid that blinking without JavaScript (only simple css and html is allowed)?

I tried to search Stack Overflow for the similar question, but with no luck.

Just added:

  • Should I "preload" the hovered image? How?
  • Should I play with z-index or opacity?

It happens with all browsers and thus the solution should work for all browsers.

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Haradzieniec
  • 9,086
  • 31
  • 117
  • 212

14 Answers14

96

Here is a simple and effective css image preloading technique I have used several times. You can load several images by placing content: url() url() url()... etc.

body:after {
 display: none;
 content: url('path/to/image-hovered.jpg') url('path/to/another-image-hovered.jpg');
}
Boschman
  • 825
  • 1
  • 10
  • 17
Kristian Svensson
  • 1,114
  • 9
  • 7
  • Love it! Great answer, doesn't require making sprites, you can dump all of your extra images in there. – Civilian Sep 23 '15 at 21:33
  • It's a good solution but does not work so well when you increase the number of icons in your application, as the number of images you load at the initial load increases every time. – yorbro Jun 19 '16 at 14:29
  • @yorbro I guess you can combine sprites with this approach – Srneczek Jun 30 '16 at 08:16
  • 4
    This wasn't working for me at first, but I realize now that for some reason the element you apply these styles to MUST be body:after! – Aaron Franke Sep 22 '16 at 06:15
  • 3
    @Aaron Franke this works for me on any element:after – Toniq Nov 10 '17 at 01:49
  • 11
    Looks like this does not work any more. My guess is that browsers optimize page loads in a way that the background for hidden elements is not loaded. Tried in recent Firefox, Safari and Chrome versions, none seem to load the image when `display: none;` is in the declaration. Same results when I try with `width: 0; height: 0`. If it is visible, it works, however adds unwanted image to the page. Anyone else facing the same? – Bence Szalai Oct 26 '19 at 16:21
  • 1
    @Grapestain same with me. Not working when there's display: none; – ladiesman1792 Nov 07 '19 at 06:14
  • 4
    this solution seems to work in modern browsers https://stackoverflow.com/a/14390213/1108467 – taylor michels Sep 10 '20 at 18:13
  • I used only width 1 and height 1 and it worked for me – josue.0 Sep 16 '21 at 23:00
  • If you have developer tools open, make sure the "disable cache" checkbox is not selected. Banged my head against this for days before I finally realized. Using this solution worked for me! – CodeNinja Mar 08 '22 at 15:09
62

The easiest way to avoid this is to make use of image sprites. For a good overview, check out this CSS Tricks article.

That way, you not only solve the flicker problem you're seeing, but will also reduce the number of HTTP requests. Your CSS will look something like:

a.class-btn { background: url('path/to/image.jpg') 0 0 no-repeat; }
a.class-btn:hover { background-position: 0 -40px; }

The specifics will depend on your images. You can also make use of an online sprite generator to make the process easier.

CherryFlavourPez
  • 7,529
  • 5
  • 45
  • 47
15

A simple trick I use is to double up the original background image making sure to put the hovered image first

.next {
  background: url(../images/next-hover.png) center center no-repeat;
  background: url(../images/next.png) center center no-repeat;
    &:hover{
      background: url(../images/next-hover.png) center center no-repeat;
    }
 }

No performance hit and very simple

Or if you're not using SCSS yet:

.next {
  background: url(../images/next-hover.png) center center no-repeat;
  background: url(../images/next.png) center center no-repeat;        
 }
 .next:hover{
  background: url(../images/next-hover.png) center center no-repeat;
 }
Callam
  • 949
  • 1
  • 14
  • 22
  • 1
    if I was developing browser Id load only the valid background (so the last one) to minimize the network load. I think you can expect this is not gonna work in any browser sooner or later. – Srneczek Jun 30 '16 at 08:09
  • 5
    this worked OK until a recent chrome update, now it's optimized by the browser (which is great, but breaks the preload) – MightyPork Oct 22 '16 at 18:14
9

If you do this:

#the-button {
background-image: url('images/img.gif');
}
#the-button:before {
  content: url('images/animated-img.gif');
  width:0;
  height:0;
  visibility:hidden;
}

#the-button:hover {
  background-image: url('images/animated-img.gif');
}

This will really help!

See here:

http://particle-in-a-box.com/blog-post/pre-load-hover-images-css-only

P.S - not my work but a solution I found :)

  • 2
    Kinda hacky, but it works. I would add position: absolute; or much better display: none; instead of visibility: hidden; if you don't want to mess your layout. – Srneczek Dec 08 '15 at 16:48
  • Doesn't work if you actually want to use the pseudo element for something – anthonygore Jul 09 '16 at 01:56
  • @Srneczek `display: none` prevents image uploading, just add `position: absolute` – Stiig Jul 27 '23 at 08:55
3

@Kristian's method of applying hidden 'content: url()' after the body didn't seem to work in Firefox 48.0 (OS X).

However, changing "display: none;" to something like:

body:after {
 position: absolute; overflow: hidden; left: -50000px;
 content: url(/path/to/picture-1.jpg) url(/path/to/picture-2.jpg);
}

... did the trick for me. Perhaps Firefox won't load hidden images, or maybe it's related to rendering(?).

hdscp
  • 31
  • 2
  • Good catch! Thank you for sharing! – Kristian Svensson Aug 31 '17 at 10:45
  • Nice! I would suggest to use `position:absolute; width:0; height:0; overflow:hidden; z-index:-1;` though. Somehow `left: -50000px` freaks me out) Basically, the problem is the same as described here: https://stackoverflow.com/a/14390213/4810382 – bilbohhh Feb 21 '20 at 14:22
2

You can preload images

function preloadImages(srcs, imgs, callback) {
var img;
var remaining = srcs.length;
for (var i = 0; i < srcs.length; i++) {
    img = new Image();
    img.onload = function() {
        --remaining;
        if (remaining <= 0) {
            callback();
        }
    };
    img.src = srcs[i];
    imgs.push(img);
}
}
// then to call it, you would use this
var imageSrcs = ["src1", "src2", "src3", "src4"];
var images = [];
preloadImages(imageSrcs, images, myFunction);
Anshuman Jasrotia
  • 3,135
  • 8
  • 48
  • 81
2

This is a non-CSS solution: if the hover images are in one directory and have a common naming convention, for example contain a substring '-on.', it is possible to select the file names and put it into the HTML as a series of:

<img src='...' style='display: none' />
Martin Staufcik
  • 8,295
  • 4
  • 44
  • 63
1

For me this worked a.class-btn{transition-delay: 0.1s;}

John Mark
  • 57
  • 8
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 14 '23 at 08:09
0

If they are the same dimensions, one possibility is to draw the two images directly on top of each other, with the CSS :hover class for the top image having display: none;

This way both images will be preloaded, but hovering will make the second visible.

asc99c
  • 3,815
  • 3
  • 31
  • 54
0

The "double up the original background image" trick didn't work for me so I used another css trick:

.next {
    background: url(../images/next.png) center center no-repeat;        
}
.next:hover {
    background: url(../images/next-hover.png) center center no-repeat;
}
.next:after {
    content: url(../images/next-hover.png);
    display: none;
}
Nailgun
  • 3,999
  • 4
  • 31
  • 46
0

This technique works nicely for me and ensures not only is the hover image pre-loaded, but it's also ready and waiting to be displayed. (Most other solutions rely on switching the background image on hover, which just seems to take the browser a bit of time to figure out, however much the image is pre-loaded.)

Create :before and :after pseudo elements on the container with the two images, but hide the one you want to see on hover. Then, on hover, switch the visibility.

So long as they both share the same size and positioning rules, you should see a neat swap.

.image-container {
    &:before { display: block; background-image: url(uncovered.png); }
    &:after { display: none; background-image: url(uncovered.png); }
}
.image-container:hover {
    &:before { display: none; }
    &:after { display: block; }
}
0

I had the same issue. After trying everything related with css i can not solve the problem. What finally solved the problem was simulating that someone hovers the element. The css is the normal one.

CSS

#elemName{
 /* ... */
} 
#elemName:hover{
 /* change bg image */
}

JS

var element = document.getElementById('elemName');
var event = new MouseEvent('mouseover', {
  'view': window,
  'bubbles': true,
  'cancelable': true
});
element.dispatchEvent(event);
phoenixstudio
  • 1,776
  • 1
  • 14
  • 19
-2

Just change the size of the background image, instead of the source of it! So...

a.class-btn {
    background-image: url('path/to/image-hovered.jpg');
    background-size: 0;
}
a.class-btn:hover {
    background-size: auto;
}
Pluto
  • 2,900
  • 27
  • 38
-3

The best way to do this is to just insert the images onto the webpage and set display to none.