JQuery Background Fader

Animating {background-image} Opacity In JQuery Without Affecting Child Elements

| 29 Comments

When first learning JQuery I was frustrated at the inability to animate a simple fade-in fade-out on images set using the CSS background-image property. Workarounds involving the appending and fading of additional HTML elements have been around for a while but prevent you from using web-text within these elements – no good for accessible or CMS editable menus.

With a bit of fiddling, I’ve come up with a solution.

Demo

Download ‘backgroundfader.js’

The Challenge

I wanted to use JQuery to unobtrusively apply a fade effect to a menu that used the CSS :hover pseudo-class to switch between two background images. The HTML should be just as you’d expect – a standard unordered list, and without JavaScript enabled, the CSS should work as normal. I also wanted the words on my buttons to be web text, rather than images of the text, so it could be enlarged (for accessibility) and edited in the source HTML (good for CMS’s, makes updating less hassle). Judging by a quick search on Google, a lot of other people wanted to do this too. There were a couple of problems to overcome however.

Problem 1: JQuery Can Only Animate Numeric CSS Values

The JQuery .animate() method can be used in conjunction with .hover() method to get CSS stuff to happen incrementally as the mouse enters or leaves an element. It only works with CSS values that are numbers, however, which is fine if you want to reposition or scale things, but no good if you want to deal with the background-image property, which relies on keywords.

There are two main solutions out there which attempt to deal with this:

Jonathan Snook came up with a clever way of animating the position of a background image made from different shapes and gradients to create a range of effects. These look good, but don’t allow you to fade between two independent images, which is what I was after.

Dragon Interactive are well known for their ‘Pufferfish’ technique which appends an additional styled <span> or <div> element to the relevant part of the DOM, which is then faded in and out on mouseIn/mouseOut. Much closer to what I required, but you can only use text created as an image, because of problem number 2…

Problem 2: Fading With Opacity Makes All Child Elements Fade Too!

opacity is a CSS property that uses numeric values to determine how transparent the selected element is. 1 = fully opaque, 0= fully transparent, 0.5 = 50% opacity. Because the values are numeric means that JQuery can alter them over time using the .animate() method. Using opacity has a major flaw however – it is inherited by all children of the selected element. This means that any text or other content contained with the faded element also fades, not desirable if you are making a menu.

Here is a demo to illustrate the problem.

Solution – With Thanks to Nick O’Brien

Nick O’Brien has a JQuery plug-in called ‘backOpacity‘ which gives you a new method to play with which allows you to apply opacity just to an element’s background. It works by appending a new <div> behind the selected element, giving it a width and height based on the selected element, and putting it in place with absolute positioning.

backOpacity() then takes a couple of parameters to specify the colour and opacity of this new background element.

This was more what I was after, but still has limitations – it doesn’t work with background-image, only background-color, and doesn’t include any options for animating the opacity value.

What I have done is taken Nick’s code and worked out a way to make it work with background-image, plus given it animation capabilities.

I’ve also tried to make the technique as unobtrusive as possible, so there is no extra markup or JS in the HTML, and with JS turned off, everything behaves as normal (i.e. you just get a simple CSS hover image switch).

Here is a demo (Works in FF, Chrome, Safari & Opera – Won’t work in IE I’m afraid Now works in IE 7+ !)

Instructions

HTML:

Hot link to JQuery on Google Code in your <head> then place the backgroundfader.js file in your site root and link to it in the <head>, after the JQuery include.

Next, create a standard unordered list menu:

   <ul id="nav">
   <li><a href="index.html">Home</a></li>
   <li><a href="info.html">Info</a></li>
   <li><a href="gallery.html">Gallery</a></li>
   <li><a href="contact.html">Contact</a></li>
</ul>

CSS:

Style the list as you would do normally, with the following exceptions:

You will need to style the <a> elements to include the ‘up state’ image as their background, and the <li> elements to include the ‘hover state’ image. a:hover{} then must set background-image to ‘none’. What this does is place the ‘up state’ image over the top of the ‘hover state’ image by default. When the cursor moves over the <a> element, the ‘up state’ image is removed, revealing the ‘hover’ image underneath.

<a> must also be given a specified width and height to allow the appended <div> to be positioned correctly. You can use % or em units, but the script will convert these to pixel measurements. This presents some limitations, which I will cover at the end.

The code used in the demo is as follows:

#nav {
margin: 0px;
padding: 0px;
list-style-type: none;
}
#nav li {
float:left;
text-align: center;
background-image: url(../images/menu-sprite.png);
background-repeat: no-repeat;
background-position: center -75px;
}
#nav a {
display: block;
width: 120px;
height:30px;
padding:10px 15px;
font-family: Arial, Helvetica, sans-serif;
font-size: 1.1em;
text-decoration: none;
color: #fff;
font-weight: bold;
text-shadow: 1px 1px 1px #666;
background-image: url(../images/menu-sprite.png);
background-repeat: no-repeat;
background-position: center top;
}

#nav a:hover {
background:none;
}

In the spirit of progressive enhancement, we could leave things at this stage, and it will still function. If users do not have JavaScript enabled, the hover effect will still work, it just won’t include the fancy fading effect.

An additional rule needs to be added to style the new appended <div> that the script will create:

#nav li div {
background-image: url(../images/menu-sprite.png);
background-repeat: no-repeat;
background-position: center top;
}

All this needs to do is add the same background image as is used for nav a.

JavaScript:

Open the backgroundfader.js file and configure the following settings:

var selector = "#nav li a";
var hoverOverSpeed = "500";
var hoverOutSpeed = "300";

Specify the element/s that you wish to apply the effect to using the selector variable

Specify the fade in/out speeds (in milliseconds) using the bottom two variables.

That’s it!

JQuery Fading Menu

What The Script Does

  • Locates the selected element/s.
  • Appends a new <div> with the same width and height to the selected element.
  • Applies CSS to absolutely position the new <div> behind the selected element.
  • Sets the selected element to {background-image:none;}, thereby revealing the <div> underneath.
  • Listens for mouseOver/mouseOut events, then animates the opacity of the <div> accordingly, revealing the <li> element underneath as a result.

Benefits

  • Menu buttons can contain live web text
  • Text can be resized and selected
  • Text can be styled with CSS (useful to create ‘selected’ or ‘active’ states)
  • To edit the button text or add new buttons, simply edit the HTML source

Compatibility

The script appears to work fine in recent versions of Opera, Safari, Firefox and Chrome. It should work fine with JQuery 1.3.2 through to the current iteration, although I haven’t tested  it rigourously on all versions.

Err… it doesn’t currently work in Internet Explorer though (surprise!). JQuery itself is giving me runtime errors (?). My script removes the styling for the <a>, but the additional <div> isn’t appended, so the buttons are permanently left in their hover state – not good!

Kudos to Gary Jones who pointed out in the comments that as of JQuery 1.4.4 this now works in IE 7 and above too.

Other Limitations

Because of the way the script works, you need to declare a fixed width and height for the selected element, and everything ends up being positioned absolutely. This means the elements cannot size to fit their content. This is just about excusable in the case of a menu, as long as the text has a lot of space around it to enlarge into, but is more problematic if you wanted to apply this technique to a standard content box.

At present, you can’t really apply this technique to elements that use ‘sliding doors’, as it requires a different way of utilising and styling the available HTML elements.

It could also be made a bit more efficient – sometimes there is a bit of lag on the fade effect.

Needless to say, these are my next challenges – unless someone else wants to come up with some fixes first!

If you enjoyed this post, please consider sharing it using the above buttons, leaving a comment or subscribing to the RSS feed.

29 Comments

  1. aw aw aw…so blink2 and so smooth
    thanks ^^

  2. Pingback: Animating {background-image} Opacity In JQuery Without Affecting Child Elements | RefreshTheNet

  3. i think i’m following this correctly, but couldn’t you use rbga() for the opacity?
    rgba will change the opacity of the background color, and only the color, not child elements.

    since it doesn’t work in IE anyway, why not use experimental CSS3 to get it all worked out.

    instead of a sprite, use :
    -webkit-gradient(….)
    -moz-radial-gradient(….)

    and then use jQuery to change the alpha channel of the rgba().
    although,looking at it, I’m not 100% sure it will do what you’re trying to do, and I don’t have enough time right now to try and get a demo up.

    • @steve – rgba will only work on solid colours, not background images.

      Yes you could replicate a similar style to that used in the demo using CSS3 gradients but this method will work with any background image that could include textures and other visual styles that can’t be achieved using gradients alone.

      It doesn’t work in IE yet, but it will once I figure out what the problem is…

  4. Hey, was just browsing through the internet looking for some information and stumbled on your blog. I am impressed by the information that you have on this blog. It shows how well you get this subject. Marked this page, will come back for more. You, my friend, ROCK!!!

  5. I copied your demo, removed the conditional comments, bumped to jQuery 1.4.4, and it works fine in IE8 and IE7 :-)

  6. This is exactly what I was looking for, except I want to animate the background of an element elsewhere on the page, not within the selector element. Is that possible with your script?

    • Not as it stands, but you could add in a conditional statement so that the function is only triggered during a mouseover event on the external element.

  7. Thanks for sharing your technique and having the due diligence to make modifications to an existing script to achieve this functionality. I appreciate it.

  8. It does its job but unfortunattely is not semantic. The empty div is superfluous.

    • Yes, the div is there for presentational purposes only, but is appended by the script that creates the effect, which makes it less semantically evil, no?

  9. I’m using this with some transparent sprites and in IE9 (and below of course :p) the element initially has a black, jagged background to it. The black disappears when the animation is initiated.

    You can see it on the big green ‘get started button’ (above the fold) on Moblized.com. Any ideas? Thanks!

    • Interesting…what I can see initially from looking at the moblized.com example that you give is that the problem lies with the overlaid element that fades, the ‘behind’ image doesn’t show the additional black pixels.

      Hard to say at this point whether it is a problem with parsing of the PNG alpha channel data, the CSS opacity property, or something to do with appending the element using JS.

      Will investigate!

  10. How I can define a menu item as active? To avoid alterations to be above the entry.

    thx

    • Best way I find is to give each <a> in the menu an id (e.g. id=”home-button”, “about-button”, etc) and also give a unique id to the <body> tag for each page (eg id=”home-page”, “about-page”, etc), then you can do this in your CSS:

      #home-page #home-button, #about-page #about-button, etc... {
      style for active buttons here...
      }
      

      The selector ensures that your ‘active’ style will only be applied to the button belonging to the currently active page.

  11. Hi James,
    I have a div called A with a background-image, and I want to place a div called B (less size that A) but setting the A opacity in 50%.
    Can you do it with this plugin? In that case what is the code I should use?
    Many thanks
    Fabian

    • @Fabian, no, this plugin would keep the size of DIV A and B the same. You would either have to alter the code in the plug-in so that it scales the size of the appended DIV, create a new background image that is the same size but has a border of transparent pixels to give the illusion of a smaller scale, or perhaps do the scaling with CSS.

      Without seeing your code I would say the middle option is probably the least amount of hassle.

  12. Is there a way to do this with a transparent first sprite and a colored second half. The effect would be to mouse over a link and have a background appear and then disappear when the mouse is removed.

    • You could fake it by having the appended <div> image mimic the appearance of the <ul> or containing element background, then reveal the <li> image that it is hiding on hover.

      • I am just trying to understand way jquery doesn’t support png alpha transparency. It would make life easier for a lot of things. Is there no way to do this?

      • The issue lies with how both Javascript and CSS work – they can transition between numerical values (from an alpha of 1 to 0, for instance, RGBa values, or from an x position of 10px to 20px) by just enumerating through the numbers in between, but there is no way of creating a transition between two image uris, which are string values (from “image1.png” to “image2.png” – can’t be done). There is also no way for JQuery or CSS to hook onto any alpha data contained within a PNG, so we are left with the opacity CSS property which has the inheritance problems as described in this post.

        What you want to do is possible using pseudo elements as in this example but only FF4+ supports this at present. The CSS3 spec for opacity also states that opacity shouldn’t be inherited, but no browser vendors have implemented this yet.

  13. rigging it with a false background would be tough if it is a gradient or patterned background.

  14. Good Stuff James. I am glad I asked you :) I tried using a semi transparent background in an accordion menu and got flogged, lol. I would word for the first open panel but when every other panel opened I just got grey. If there are any developments please keep us informed.

  15. hya, first of all thanks for sharing this script.
    I am using it in wordpress, and i have no problem except this one:
    i just cant set specific background for current page item, and i think its cause of the opacity thats set by the script. i can set everything in current page item except background (control text color, decoration etc..)
    thanks in advance

    • Hi Dino

      You need to force the appended <div> to have an opacity of 0 so it reveals the background of the <li> sitting underneath.

      #nav li.current-menu-item div {opacity: 0;}
      

      (Not sure about that selector but it’ll be something like that.)

  16. hya again
    thank you for response, but that doesnt do it, cause in js you defined that opacity to be when not hovering: 1. So it gets overwritten by it and is always 1. At least thats in my case.

  17. Ok now i got it to work by putting opacity: 0!important;
    but that option doesnt work in ie. If you have any idea that would be great :)
    Thank you for your time

  18. Hi! I was trying to deal with this problem. I found this post, start reading it, and… oh! Neat solution! Works 100% OK in a little piece of code. Thanks for share!

Leave a Reply

Required fields are marked *.

*


Spam protection by WP Captcha-Free