Pressure is nothing more than the shadow of great opportunity. - Michael Johnson

Dynamic Picture Captions with JavaScript

3 years, 10 months ago

Introduction

This post explains how to dynamically add captions to all pictures with a specific class applied. Text for the captions is taken from each pictures' title attribute. It also covers absolute positioning of an element relative to its container (rather than the page as a whole) and CSS opacity support in mainstream browsers. This is used to allow pictures to partially show through the background of their captions.

Example 1 - a static version of what we're trying to achieve.

First Steps

We need to set up a simple web page containing some test pictures. I've grabbed a few from my Flickr account. A class of "caption" will act as a hook allowing us to choose which pictures the JavaScript replacement routine should apply captions to.

Example 2 - the basic HTML we'll be restyling.

In this example the choice of container, in the case an unordered list, for the images is arbitrary. If used in something like a photo gallery application this would probably be a good choice from a semantics point of view.

We'll add some basic styling to the list to hide the bullets for each item and reset the paddings and margins to ensure the images sit nicely against the left hand edge of the page. A one pixel border helps to make the pictures stand out.

An Overview of The Replacement Method

We'll use JavaScript to find all image elements with a class of "caption" applied. We'll then substitute them with DIV elements which we can create using DOM methods. In each case the substituted image will be added as a new child node of the DIV element. We'll create a new paragraph which will also be added as a child node. This paragraph will be used for the picture caption.

The "caption" class needs to be "moved" from the image to the container. This will ensure the border and any other styling is removed from the image and appropriate rules applied to the container instead. The generated HTML we're aiming for is shown below:

<div class="caption">
   <img src="images/image-1.jpg" width="500" height="374" alt="A plate of pasta">
   <p>Really delicious food</p>
</div>

CSS Rules

Having created this structure we need to add CSS rules to position the caption and style it appropriately. The caption has to "stick" to the bottom edge of the container, and by implication the picture, regardless of picture dimensions or font size increases.

Absolute Positioning

The CSS position property allows us to specify absolute positioning. Setting position: absolute on the paragraph allows us to use top, left, right and bottom CSS properties to control its position. By default absolutely positioned elements takes their starting points from the page itself, i.e. an element positioned at top: 0; left: 0; would be displayed at the top left hand corner of the browser viewport. By specifying position: relative; on an element's parent we can force it to take its starting points from that parent instead. The properties top: 0; left: 0; then become the top left hand corner of the parent container instead. For our example we need to anchor the caption against the bottom edge of the container. Using these principles the following CSS achieves this:

div.caption {
   position: relative;
}
div.caption p {
   position: absolute;
   left: 0;
   bottom: 0;
   padding: 7px;
   margin: 0;
}

Strictly speaking left: 0; shouldn't be necessary but Internet Explorer won't work correctly if we don't add this rule.

Some padding improves the look of the caption and the default paragraph margins need to be zeroed. You may also notice that the class selectors have been specifically prefixed with the element type, in this case DIV. As we'll be applying the "caption" class to both the original image elements and the DIV elements which replace them this allows us to target and apply different rules to each. In this way we can easily add rules to be applied against the images when JavaScript is disabled and a different rules for use when JavaScript support is available.

Width Issues

Having applied absolute positioning you'll notice that the captions no longer stretch to the full width of the container. As with floated elements, those with absolute positioning shrink wrap to fit their contents. We can solve this problem by applying a width. Of course in our final code we'll have to assign this using JavaScript as we won't know the width of our pictures upfront.

Box Model Problems

For browsers which follow the W3C box model recommendations we'll have to calculate the width as picture width - left padding - right padding. Assuming use of a valid doctype this will apply to all modern browsers. Strictly, this isn't completely true but for the doctypes one's ever likely to use it's safe to assume this. For IE 5.5 and below which always implement Microsoft's alternative box model we'll simply set the caption width to the width of the picture.

CSS Opacity

The captions need to be semi-transparent to allow the picture underneath to partially show through. We'll start by setting a background colour for the captions:

background: #ccc;

CSS3 provides support for an opacity property. This takes numeric values in the range 0.0 (fully transparent) to 1.0 (fully opaque). This property isn't supported in every browser but fortunately there are a couple of other browser specific rules we can apply alongside this one which enables us to implement opacity in the latest version of every mainstream browser. The following CSS achieves the effect we're after:

div.caption p {
   opacity: .5; /* Firefox 1.5, Safari, Opera 9 */
   -moz-opacity: .5; /* Firefox 1.0.x */
   filter: alpha(opacity=50); /* IE */
}

The proprietary Internet Explorer filter property works slightly differently. It takes a value between 1 and 100. In this example 0.5 and 50 represent the same level of transparency. The table shown below summarises which properties are supported by each browser.

Rule / Browser IE 5.0 IE 5 Mac IE 5.5 IE 6 IE 7 Firefox 1.5 Opera 9 Safari 2
opacity: .5 No No No No No Yes Yes Yes
-moz-opacity: .5 No No No No No Yes (and 1.0.x) No No
filter: alpha(opacity=50) Yes No Yes Yes Yes No No No

It's worth pointing out that Opera 8.5, which has only recently been replaced by version 9, doesn't support any of these properties. We'll have to accept a solid colour caption when viewed in Opera 8.5.

If you don't mind setting opacity with JavaScript then the YUI Library standardises the setting of this style taking care of the browser discrepancies for you. Simply use the following line in the JavaScript described below will achieve the same effect:

YAHOO.util.Dom.setStyle(oCaption, 'opacity', 0.5);

Writing the JavaScript Method

We'll define our JavaScript method within an object literal. It will contain one method - init which will be instantiated on page load using the YUI Library YAHOO.util.Event.addListener method. Here's the skeleton code:

var oImageCaptions = {
   init: function() {

   }
}

YAHOO.util.Event.addListener(window, 'load', oImageCaptions.init);   

We start by finding all image elements with class "caption" applied. The YUI Library provides a suitable method for this.

var aImages = YAHOO.util.Dom.getElementsByClassName('caption', 'img');

Next we'll loop through this collection.

for (var i = 0, j = aImages.length; i < j; i++) {
   
}

We assign the length of the collection to a variable upfront. This is more efficient as the length isn't being recalculated on each iteration of the loop.

For each image we need to make sure that a title has been assigned. There's not much point going to the trouble of setting up a caption if there's no text to display.

if (aImages[i].title != '') {
   
}

All substitution code will be embedded within this if statement.

We need to set up some variables up front:

var oParent = aImages[i].parentNode;
var sTitle = aImages[i].title;
var sClasses = aImages[i].className;

The first line retrieves the current image's immediate parent. We'll need this when we replace the image with a new container later. The last two lines store the image's title and any assigned classes.

Now we've tucked those values safely away we can clear the associated image properties:

aImages[i].title = '';
aImages[i].className = '';   

We're finished with setting things up now. On to creating new elements. The following line creates the container:

var oContainer = document.createElement('div');

At this stage it doesn't exist within the DOM hierarchy. It's in limbo, waiting to be added to a suitable parent. We'll add it later. We've got a few things to do before then.

Earlier we saved a copy of classes assigned to the image. The following code assigns them to the container we just created:

oContainer.className = sClasses;

In this example, I've explicitly chosen to move all classes from the image element to the container so that we can easily preserve features such as floating after the JavaScript method has run. If, on the other hand, you wanted to preserve classes on the image you could always use the YUI YAHOO.util.Dom.removeClass and YAHOO.util.Dom.addClass methods to selectively remove only the "caption" class and add to the container.

We need to ensure the container sits snugly around the image. To achieve this we'll set the width and height of the container to the same as the image.

oContainer.style.width = aImages[i].getAttribute('width') + 'px';
oContainer.style.height = aImages[i].getAttribute('height') + 'px';

Before creating the caption we'll need to replace the image with the newly created container in the DOM.

oParent.replaceChild(oContainer, aImages[i]);  

We'll then add the image back into the DOM as a child of the container.

oContainer.appendChild(aImages[i]);

We can now create a caption:

var oCaption = document.createElement('p');
oCaption.appendChild(document.createTextNode(sTitle));

The first line creates a new paragraph element. The second creates a text node to hold the contents of the paragraph and adds it as a child node of that paragraph. Finally we add the paragraph as a child of the container.

oContainer.appendChild(oCaption);

As mentioned previously absolutely positioned elements, like this paragraph, shrink wrap their contents. We'll need to set the width of the paragraph to correct this. In browsers which follow W3C box model recommendations we'll need the following code:

var iPadLeft = YAHOO.util.Dom.getStyle(oCaption, 'padding-left').replace('px', '');
var iPadRight = YAHOO.util.Dom.getStyle(oCaption, 'padding-right').replace('px', '');

oCaption.style.width = (aImages[i].getAttribute('width') - iPadLeft - iPadRight) + 'px';

In IE 5.5 and below and IE 6 in quirks mode we'll need the following code instead:

oCaption.style.width = aImages[i].getAttribute('width') + 'px';   

We'll need to work out which code to send to the browser. We could use browser sniffing to detect IE. The following check might work:

if (navigator.userAgent.indexOf('MSIE 5')) {
   
}

Of course we shouldn't use browser sniffing, it's just not nice. We might inadvertently target a browser which pretends to to be IE 5. Earlier versions of Opera are the biggest offender in this respect but there are probably others. The following code might seem safer but we're now starting down a slippery path.

if (navigator.userAgent.indexOf('MSIE 5') && navigator.userAgent.indexOf('Opera')) {

}   

Let's stop and take a different direction. A better bet would be to detect support for features which we know are specific to IE. It supports a proprietary document.all method. Checking for this will determine if we're using any version of IE. We can combine this with a check for document.compatMode which was introduced in IE 6. It's also supported by Firefox but this doesn't matter as we're also confirming the existence of document.all. If the property doesn't exist or it does and its value is set to "BackCompat" the browser is running in quirks mode and, in the case of IE, is using it's own proprietary box model.

Actually it turns out that Mozilla browsers also implemented document.all to provide backwards compatibility with older scripts which relied on it. This would throw a spanner in the works but fortunately it's implemented silently. It works but if you test for its existence it'll return false. Our code for setting the width now looks like this:

if ((!document.compatMode || document.compatMode == 'BackCompat') && document.all) {
   oCaption.style.width = aImages[i].getAttribute('width') + 'px';
} else {
   var iPadLeft = YAHOO.util.Dom.getStyle(oCaption, 'padding-left').replace('px', '');
   var iPadRight = YAHOO.util.Dom.getStyle(oCaption, 'padding-right').replace('px', '');

   oCaption.style.width = (aImages[i].getAttribute('width') - iPadLeft - iPadRight) + 'px';
}   

A third way, and in my opinion the nicest, makes use of offsetWidth. IE in quirks mode will report that the width and offsetWidth have the same values. All other browsers including IE in standards mode will report the offsetWidth as being equal to the width + left padding + right padding. We can therefore use the following code which sets the width to the same as the image and then compares that to offsetWidth and adjusts accordingly to take account of the two different box models:

var iImageWidth = aImages[i].getAttribute('width');
	
oCaption.style.width = iImageWidth + 'px';
	
if (oCaption.offsetWidth > iImageWidth) {
	oCaption.style.width = iImageWidth - (oCaption.offsetWidth - iImageWidth) + 'px';
}   

Finally, we could of course avoid the whole box model issue and the need to detect the browser by instead adding a DIV around the paragraph. This would allow us to set the width on the DIV and the padding on the paragraph.

I've added additional list items containing text between each of the images to demonstrate that the replacement is happening in the right place in the DOM.

Further Reading

Notes

  • I've tested the examples in Firefox 1.5, Safari 2, Opera 9, IE 5.5 & IE 6.
  • Thanks to Christian for his suggestions and for giving the post a quick review prior to my putting live.

Comments

  • Since you're using the YUI library anyway, why not use their opacity filter?

    YAHOO.util.Dom.setStyle(['test', 'test2'], 'opacity', 0.5);

    Brad Wright - 17th October 2006 #

  • Hi Brad - welcome.

    I don't like the idea of setting what is essentially styling information directly in the JavaScript.

    Anyway from Firefox 1.5 it's only two CSS rules to cover all A-Grade browsers anyway.

    Ed Eliot - 17th October 2006 #

  • I need help. How to fetch Image from CSS file.

    Karthikeyan - 11th December 2006 #

  • Karthikeyan - can you give more details of the problems you are experiencing?

    Feel free to send me an email.

    Ed Eliot - 11th December 2006 #

  • what a great example!! thanks

    nick99 - 25th January 2007 #

  • hey this post is really helpful

    But, it's critical to note. offSetWidth and width don't return the same value.

    If you have a border applied to the image, then the offSetWidth will add that value into the padding left + image + padding right.

    Essentially, an image whose actual width is 120px with a 3px padding and a border would return 128 via offSetWidth. But 126 will be returned via width. Of course this is only true for IE. In firefox the width will be 120px - the expected, and generally desireable result

    One question, do you know how to dynamically get the padding-right, padding-left for IE? I need to grab the width and height of all images where the padding and border aren't consistent for all images on the page.

    thanks again -gregory

    gregory - 1st March 2007 #

  • Gregory - They'll return different values for any browser which implements the W3C box model - everything except IE 6 running in quirks mode and IE 5.5 and below. For IE 6 running in standards mode they will also be different. The point of comparing offsetWidth and width is to determine which box model the browser is using and adjust the overall width if necessary in a way which factors in both padding and borders where applicable.

    The easiest way to get the padding-left and padding-right would be to use the YUI YAHOO.util.Dom.getStyle method.

    Ed Eliot - 1st March 2007 #

  • http://www.photo.com

    ali reza - 10th May 2007 #

  • Hey, this is a great read. Any reason why you prefer the YUI? JQuery is lightweight and pretty powerful.

    Cocoliso - 13th June 2007 #

  • This is kind of cool and in-depth, Ed, although I think this is a much simpler way to achieve a similar effect.

    Aljazeera.net uses this technique very effectively.

    Gerard - 21st May 2008 #

  • Could the script be adapted to work with multiple areas on an image map (using usemap), with each caption appearing at the bottom of the defined areas?

    Ian Tresman - 1st October 2008 #

  • Im using YUI based panel , in which its works fine for IE, not on other browser like firefox n netscape

    /***************** My Code *************/

    //Initializing panel YAHOO.namespace("example.container"); function init() { YAHOO.example.container.panel = new YAHOO.widget.Panel("panel", { width:"456px", visible:false, draggable:false, constraintoviewport:false } ); YAHOO.example.container.panel.render(); } YAHOO.util.Event.addListener(window, "load", init);

    //calling panel function TestObj(id) { YAHOO.util.Event.onAvailable(id, this.handleOnAvailable, this); } TestObj.prototype.handleOnAvailable = function() { showpanel(); } function showpanel(){ YAHOO.example.container.panel.show(); } var obj = new TestObj("myelementid");

    can someone help me on this??

    thanks in advance.

    dsenthil - 3rd December 2008 #

  • Hey, this is a great read. Any reason why you prefer the YUI?

    Ed Halbert - 5th December 2008 #

  • Great post, learning about js can save us so much time, thanks a lot. Time is passing, but many articles like this one stay up to date by themselves!

    chicken - 10th September 2009 #

  • chat | Porno izle | sex izle | sicak sohbet Dear Admin, I thank you for this informative article. And I thank you for this I follow your vendors. It’s verry good. I wish you continued success whould you like. sicak chat | sohbet | chat | sohbet siteleri | chat siteleri | sohbetim | sohbet siteleri | This is a great resource for growing your buisness.There are various aspects in buiness management and to grow the business.This is a very useful for tool for young entepreneurs. Thanks You Admin chat siteleri | sohbet odalari | chat odalari - sohbet - sohbet kanalları - chat kanalları - mynet sohbet | Chat Sitesi

    sohbet - 4th August 2010 #

  • # wasn't paying enough attention to them (a classic case of not RTFM). I wish all projects did it this well. # Being able to query models from the python command line shell. It also doubles as a fairly quick (and I guess scriptable) way to add test data to your database.

    sohbet siteleri - 19th August 2010 #

  • İzmirliler için devam ...

    izmir chat - 20th August 2010 #

  • thanks good blog

    hi5 - 31st August 2010 #

Help make this post better

Notes: Standard BBCode for links, bold, italic and code are supported. rel="nofollow" is added to all links. Your email address will never be displayed on the site.

Back to index