Access options:

Cross-fading disjointed image rollover

author: mike foskett uploaded: 28th September 2008

updated: 3rd October 2008

updated: 16th October 2008

This advanced technique uses CSS-sprites and JavaScript to change an image which is not directly associated to the link. It uses accessible and clean mark-up, with unobtrusive JavaScript, and a little CSS. Content, presentation and behaviour are completely separated. The JavaScript acts on the XHTML using the style sheet.

The JavaScript is configurable and completely reusable by changing four variables.

Demonstration

This example was developed for the real-world and may be seen on the Tesco Direct website. The brief had specific requirements:

  1. Accessible 100% unobtrusive scripting.
  2. First button link locked on. Done via CSS, very easily changed.
  3. Rollover action on the other button links. All via CSS.
  4. Disjointed image locks-on until another button link is hovered over. I'll make this configurable at a later date.

A bare-bones example is available as is a complete set of files which includes minified [1.78KB] versions and graphics. Or just look at the commented JavaScript.

How it works

Using CSS-sprites there are only three images in total:

  • The default single image. "top.jpg" - the default image which loads from the style sheet.
  • Buttons sprite layout. "buttons.jpg" - The four buttons, with rollovers, in one sprite. Buttons in order one above the next with the rollover version to the right. All controlled via the CSS.
  • Disjointed image sprite layout. "top.sprite.jpg" - The final image is a CSS-sprite containing all four of the disjointed rollover images. Presented in order, one above the next. It is preloaded by the JavaScript. The name and location of the sprite is stripped from the default image attributes. It just has ".sprite" added. So ensure it is named accordingly and placed in the same folder.

Once the page has fully loaded the JavaScript kicks in and pre fetches the disjointed rollover image. The secondary images are only displayed once completely loaded thus preventing white, no-image flashes on slow connections. Well IE anyway, Firefox seems to have issues when changing background images.

For fading purposes an overlay is unobtrusively added to the disjointed image container. Both overlay and container have their background-image changed to "top.sprite.jpg" ready for manipulation.

On rolling over a button, the overlay sprite is repositioned and faded in over the container image. The containers' sprite is then repositioned in background, and finally the overlay is set transparent ready for the next rollover.

Semantic XHTML sir?

Very simple semantic mark-up:


<div id="sectionTop" class="top1">
  <ul>
    <li><a id="top1" href="#1">link 1 accessible text</a></li>
    <li><a id="top2" href="#2">link 2 accessible text</a></li>
    <li><a id="top3" href="#3">link 3 accessible text</a></li>
    <li><a id="top4" href="#4">link 4 accessible text</a></li>
  </ul>
</div>

Required:

  • All id's must be unique.
  • Id name of the disjointed image container is required by, but changeable in, the JavaScript function call.
  • The disjointed image container also requires the className of the default link id. Specifically for its href property, do if clicked before a rollover it has somewhere to go.
  • Id names on the links are required by both CSS and JavaScript. The names may be changed but the change must be reflected in the CSS which controls the sprite position. The JavaScript doesn't care what they are called so long as they're present.
  • For accessibility reasons the links should fully describe the destination.

It is not a requirement for the example navigation list to be within the disjointed image container. Consequently, with adjustments, this would also work:


<div id="disjointedImageDiv" class="firstLink"></div>

<ul id="navigation">
  <li><a id="firstLink" href="#">1</a></li>
  <li><a id="secondLink" href="#">2</a></li>
  <li><a id="thirdLink" href="#">3</a></li>
  <li><a id="fourthLink" href="#">4</a></li>
</ul>

A little style goes a long way

Because of the use of CSS sprites the style sheet is quite complex.

Global reset


#sectionTop * {
  margin:0;
  padding:0;
  border:0 solid;
  list-style:none
}

The disjointed-image: layout and positioning

This is the business end of the CSS. The CSS sprite, name and location is obtained from #sectionTop background-image


#sectionTop {
  background-image:url(top.jpg);
  background-repeat:no-repeat
  position:relative;
  width:476px;
  height:120px;
  overflow:hidden;
  margin:1em auto;
}

/* Disjointed image positioning (CSS-sprite) */
.top1 {background-position:0 0}
.top2 {background-position:0 -122px}
.top3 {background-position:0 -244px}
.top4 {background-position:0 -366px}

The link buttons

Not exactly part of the tutorial, just added for completeness


/* Buttons layout */
#sectionTop ul {
  position:absolute;
  width:100px;
  height:120px;
  top:0;
  right:0
}

#sectionTop a {
  background-image:url(buttons.jpg);
  display:block;
  width:100px;
  height:30px;
  text-indent:-200em
}

/* Button graphics positioning (CSS-sprite) */
a#top1 {background-position:100px 0} /* locked-on as per brief */
a#top2 {background-position:0 -30px}
a#top3 {background-position:0 -60px}
a#top4 {background-position:0 -90px}

a#top1:hover,
a#top1:focus {background-position:100px 0}
a#top2:hover,
a#top2:focus {background-position:100px -30px}
a#top3:hover,
a#top3:focus {background-position:100px -60px}
a#top4:hover,
a#top4:focus {background-position:100px -90px}

The overlay required by the JavaScript

Used by the JavaScript to cross fade the image sprites.


#overlay {
  position:absolute;
  width:371px;
  height:127px;
  top:0;
  left:0;
  cursor:pointer
}

It's all in the scripting

Going to start with the set-up for the example then delve into the coding depths. With that exception each section is a continuation of the previous, split for clarity.

Set-up


function setup(){
  /*
    id of disjointed object,
    id of object containing the links,
    id of the overlay to be created,
    extension of disjointed image sprite
  */
  disjointedRollover('sectionTop','sectionTop',"overlay",".sprite")
}

// test DOM functions are supported and run setup on page load
if (isDom()){
  addLoadEvent(setup);
}

Standard functions

These are part of my standard function set:


/* author: Simon Willisons - http://simon.incutio.com/archive/2004/05/26/addLoadEvent */
function addLoadEvent(f){var o=window.onload;if(typeof window.onload!='function'){window.onload=f}else{window.onload=function(){o();f()}}}

// standard functions
function $id(id){return(document.getElementById(id)?document.getElementById(id):false)}
function idExists(id){return($id(id)?true:false)}
function isDom(){return (document.getElementById&&document.getElementsByTagName&&document.createElement)?true:false}
  • addLoadEvent() - Starts the JavaScript after the page loads.
  • $id(id) - Short form for document.getElementById('id').
  • idExists(id) - Checks an id exists.
  • isDom() - Tests the DOM functions used are supported.

Set opacity function

A cross-browser, cross platform solution for setting the opacity of an id'd object:


function setOpacity(id,opacity){
  var obj=$id(id).style;
  obj.opacity=opacity/100;
  obj.MozOpacity=opacity/100;
  obj.KhtmlOpacity=opacity/100;
  obj.filter="alpha(opacity="+opacity+")";
}

Get a CSS property value

Very useful for pulling a property value from a style sheet.

Not 100% for grabbing values of all properties. Based on "Retrieving CSS styles via JavaScript" by Steffen Rusitschka. See his article for more info.


/* author: Steffen Rusitschka - http://www.ruzee.com/blog/2006/07/retrieving-css-styles-via-javascript/ */
function hyphenToCamel(s){for(var exp=/-([a-z])/;exp.test(s);s=s.replace(exp,RegExp.$1.toUpperCase()));return s;};

function getStyleProperty(id,property){
  // note this function is not 100% generic for all CSS properties
  var obj=$id(id),value='';
  if(window.getComputedStyle){
    value=window.getComputedStyle(obj,null).getPropertyValue(property);
  }else{
    if(obj.currentStyle){
      value=obj.currentStyle[hyphenToCamel(property)];
    }
  }
  return value;
}

The main routine

Parameters:

  • id - 'sectionTop' in the example. The id of the disjointed-image area.
  • linkContainer - The id of the links block, 'sectionTop' in the example.
  • overlayID - 'overlay' in the example.
  • spriteExt - '.sprite' in the example.

function disjointedRollover(id,linkContainer,overlayID,spriteExt){
  if (idExists(id)){

    // get original background-image name
    var newImg=getStyleProperty(id,'background-image');

    //remove text styling "url()"
    newImg=newImg.replace('url(','').replace(')','');

    // remove IEs ""
    newImg=newImg.replace(/"/g,'');

    // replace .extension with .sprite.extension
    var ext=newImg.substring(newImg.lastIndexOf("."));
    newImg=newImg.replace(ext,spriteExt+ext);

    // preload rollover image and attach variables
    var img=new Image();
    img.overlayID=overlayID;
    img.spriteExt=spriteExt;
    img.disjointID=id;

When the disjointed-image sprite has loaded

Don't do anything until the sprite has loaded but then:


    img.onload=function(){

      // image loaded so replace background image
      $id(id).style.backgroundImage="url("+img.src+")";

      // create an overlay span for fading
      var sp=document.createElement('span');
      sp.id=img.overlayID;
      sp.className=$id(id).className;
      sp.style.backgroundImage="url("+img.src+")";

      // main graphic acts as a link too
      sp.onclick=function(){
        window.location=$id(this.className).href;
      }

      //  add overlay to disjoint-image block 'sectionTop'
      $id(id).appendChild(sp);

The button mouse actions

Attach mouse actions to the links in the links container. The fade is done from here too. Note no fade-out just fade-ins. The timing of which is not variable, deemed beyond brief. Currently it has five opacity steps [0, 25%, 50%, 75%, 100%] timed at [0, 250ms, 500ms, 750ms, 1s]. I leave amendments to you.


      var rollover=function(){
            var obj=$id(img.overlayID)
            // set overlay initial opacity and position (via class)
            setOpacity(img.overlayID,0);
            obj.className=this.id;

            // fade in overlay: adjust 101 for fade steps, currently there's 4.
            for(var i=25;i<101;i+=25){
              setTimeout("setOpacity('"+img.overlayID+"',"+i+")",i*2);
            }

            // set bg img position
            setTimeout("$id('"+img.disjointID+"').className='"+this.id+"'",i*2);

            // switch off overlay
            setTimeout("setOpacity('"+img.overlayID+"',0)",i*2);
      }

      // add mouseover actions to links
      var liAs=$id(linkContainer).getElementsByTagName('a');
      for (var i=0;i<liAs.length;i++){
        liAs[i].onmouseover=rollover;
        liAs[i].onfocus=rollover;
      }

    }
    img.src = newImg;
  }
}

Update history

3rd October 2008 - Test for DOM functions added and the initial on-click repaired.

16th October 2008 - Reworked for keyboard only and added missing graphic to zip file. Thanks Patrick H. Lauke at splintered for raising the issue.

Any issues or ideas please email me mike dot foskett at this address

Going further

The action of locked-on is set in the code. I'll try to allow optional actions as time allows.