Automatic image captions with unobtrusive JavaScript
You’ve probably heard of unobtrusive JavaScript. It’s a simple technique that lets you create richer documents without cluttered markup or accessibility problems. I often see link decoration and drop-shadow effects done with unobtrusive JavaScript. How about unobtrusive image captions? Instead of writing a caption into the page, let JavaScript do it. In this article I’ll show you how simple this can be.
The idea
How often have you seen websites that use a table or a div to enclose an image and some text to create a “captioned” image? Far too often, I’d guess. Semantically speaking, though, nothing in the markup actually associates the image and the “caption” — this isn’t a good way to give an image a caption. Until HTML itself actually has a true mechanism for a captioned image, or until someone makes a microformat for it, you may be better off taking the path of compromise between richness and semantic content.
That compromise, in my view, is using unobtrusive JavaScript to add richness to the site after it is loaded. The document is delivered to the browser in plain-Jane fashion, then altered after the browser loads it. The end result is no more semantic than hard-coding all the extra markup, but it’s a good trade-off in my opinion.
Plus, it makes authoring content much easier. Let’s see how.
The old way
Here’s typical code you might see to create a “captioned” image:
<div style="width:100px">
<img src="treefrog.png" alt="A tree frog"
width="100" height="100" />
<p>A tree frog</p>
</div>
You may also see inline styles, a table instead of a div, and other ugliness.
The new way
The better way to do this is simply eliminate all that redundant typing. What’s redundant, you say? Well, the width of the wrapper element and the text of the “caption,” to mention two things. The image already has a width — why repeat it in the wrapper element? The image already has a place for associated text in its title attribute — why write it out again?
Let’s strip things down to the bare minimum:
<img src="treefrog.png" alt="A tree frog"
title="A tree frog" class="figure"
width="100" height="100" />
Does that look minimal to you? If you’re thinking the alt attribute and the title attribute are redundant, I disagree. The alt attribute is never to be presented to the user when the image can be presented. It is an alternate representation of the content when the image isn’t available. It is not a title for the image. That’s why there is a title attribute for images! If it were redundant to the alt attribute, it wouldn’t be there.
I propose this as the bare minimum for an image, except for one thing: CSS styling. You may want to use CSS to float your image to the left or right. Nobody wants an ugly break in the text flow for an image. But that’s where I’ll start with this article.
CSS styling
Time for a demonstration of the starting point. I’ll insert two images from stock.xchng, a great free stock photography site. I’ll float one of them left and the other right with the following CSS:
.left {
float: left;
margin-right: 5px;
}
.right {
float: right;
margin-left: 5px;
}
You can see a demonstration here. It’s pretty plain, isn’t it? But it works. Nothing fancy, but then again you can do a lot with this basic markup. Now let’s add some captions to those images.
The magic
It’s actually really easy to lift the images out of the page, insert a wrapper element, and put the image back into the wrapper. There are some subtleties, and I’ve done a thing or two to make the technique easily extensible. Here’s the code:
function addCaptionsToImages() {
wrapImagesInDiv( 'figure', [], [ 'float' ] );
}
if(typeof window.addEventListener != 'undefined') {
window.addEventListener('load', addCaptionsToImages, false);
}
else if(typeof document.addEventListener != 'undefined') {
document.addEventListener('load', addCaptionsToImages, false);
}
else if(typeof window.attachEvent != 'undefined') {
window.attachEvent('onload', addCaptionsToImages);
}
function wrapImagesInDiv( className, attributes, styles ) {
var images = document.getElementsByClassName(className);
for ( var i = 0; i < images.length; ++i ) {
var img = images[i];
// Lift the image out of the page and insert a div under it.
var parent = img.parentNode;
var frame = document.createElement('div');
var txt = document.createTextNode(img.getAttribute('title'));
parent.insertBefore(frame, img);
parent.removeChild(img);
frame.appendChild(img);
frame.appendChild(txt);
// These are special cases. We always copy these from the image to the
// div.
frame.style.width = img.getAttribute('width') + 'px';
frame.className = img.className;
// Copy specified attributes and style properties from the image to the
// div.
for ( var j = 0; j < attributes.length; ++j ) {
frame.setAttribute(attributes[j], img.getAttribute(attributes[j]));
}
for ( var j = 0; j < styles.length; ++j ) {
frame.style[styles[j]] = img.style[styles[j]];
}
}
}
I’ll break that down a bit at a time. The first few lines define a function to be called on page load. I use Scott Andrew’s cross-browser onload functionality to add an event that’ll call the wrapImagesInDiv function with the desired arguments. I do this because I want the flexibility of passing desired arguments.
Next I define the wrapImagesInDiv function itself. This function accepts a class name, an array of attribute names, and an array of style property names. The class name defines the target elements: everything with a class of “figure.” The array of attribute names specifies which attribute values should be copied from the image to its wrapper, and likewise with style properties. You’ll see where I use those in a bit.
The first line inside the function uses the document.getElementsByClassName method supplied by the Prototype library to find all the images labelled with class="figure". There are other ways to do this, but I think Prototype is ubiquitous enough that this is a fine choice.
After that, the script simply loops through the array of resulting objects. For each object, it gets a reference to the parent element, creates a div element, moves the image inside that, and appends the image’s title attribute as text afterward.
Then it does a couple of tricky things. First, it sizes the wrapper horizontally to match the image. This is necessary so the wrapper doesn’t take over the entire width of the page. Next it copies the image’s class to the wrapper. This is necessary to preserve any CSS styling that was used to place the image relative to the text. In my case, this preserves the left-floating and right-floating I created in the first example.
Finally, it loops through every other attribute and style property I specified, and copies those from the image to its wrapper. This is an easy way to get the same script to do lots of different things — you just give it different arguments. Do you want the wrapper to have the same title attribute as the image? Fine, just add ‘title’ to the argument array — I’m all about choice.
I’ll add a couple extra lines of CSS to make things prettier, too. Here’s the result: automatic image captions without ugly markup.
Further tweaks
This is a really simple technique, and you can go much further with it. Here are some more ideas:
- Use Roger Johansson’s technique to add custom corners, borders, and drop shadows. Why not?
- In keeping with thinking of the images as “figures,” auto-number them, and create references to them on the fly by adding in other hooks for JavaScript.
- Tables could be figures, too, and you could auto-number and auto-reference them. But don’t auto-caption tables, because they already have a caption element of their own.
- Generate a table of figures for your document.
If you haven’t done much reading on unobtrusive JavaScript, you really should. You can do so much with it. And the best part is, it lets you keep your HTML sooooo much cleaner and saves you so much work.
Conclusion
I hope this article has helped you break out of a common web design rut and snazzy up your documents without adding un-semantic markup bloat. If you thought this was good, let me know! If you want to suggest some improvement, let me know that too! And if you want to subscribe to my articles, go right ahead. It’s free and easy.



Wow, there’s something to be said for finishing my drafts in less than a year (yes, I started this one in October 2005). Looks like I’m not the only one with this idea.
Xaprb
4 Nov 06 at 5:50 pm
Hi there!
Have been playing around with this code, and it comes in very handy for my new website. However, I am using this in thumbnail images which already have the <a > tag around them. When I use this script, the < div > gets created inside the < a > tag, causing Internet Explorer to have problems with the image link!
So, this is the situation:
- starting HTML: < a > < img > < /a >
- automatic caption code makes it: < a > < div > < img > < /div> < /a >
- how do I make it: < div > < a > < img > < /a >< /div>
?
I hope you can help me out – I have no clue about Javascript and have no idea how to let Javascript point to the < a > tag surrounding the < img > tag which contains the “figure” class. Thanks in advance:)
Framboos
8 Dec 06 at 9:27 pm
You should read the DOM spec from the W3C to understand how to use this — but the A tag your IMG tag is wrapped in is its parent. So you would change the JavaScript to look up the element’s parent and wrap that, instead of the A itself. You can see how this works just by examining the existing code. (Look for var parent = img.parentNode; and go from there).
Xaprb
9 Dec 06 at 8:03 pm
Yippee! I spent way too long – more than an hour?? – trying to grasp this and when I had almost given up, it worked out all ok!
The code looks like this now – which perhaps isn’t the most elegant solution? But it works! I’m very happy now :) Thanks!
var parent = img.parentNode; var grandparent = parent.parentNode; var frame = document.createElement('div'); var txt = document.createTextNode(img.getAttribute('title')); grandparent.insertBefore(frame, parent); parent.removeChild(img); grandparent.removeChild(parent); frame.appendChild(parent); parent.appendChild(img);Framboos
9 Dec 06 at 9:49 pm
Thanks for this post. I thought I would let you know about a couple of problems that I had with this code on a new site I am building and how I solved them. I am using this technique with a CMS that uses jquery (Drupal V5.0) and not the prototype library. I changed this line to get it to work with the DOM traversal in Jquery:
var images = document.getElementsByClassName(className); //to var images = $('img.figure');I am using IE6 SP2 and I had a problem with some additional whitespace on the right hand side of the images. It was like I had more padding on the right hand side.
On IE7 this space wasn’t as noticeable, however if you had a long title the first word would sometimes appear on the right hand side of the image instead of underneath it. On your demonstation page in ie6 I see more whitespace on the right hand side of your images. When I look at your demo page with IE7, there is no padding at all on the right hand side of the lower image. I found that these problems were fixed by concatenating a space to the start of the title. This space doesn’t show up in the caption under the image. I did this with the following line:
var txt = document.createTextNode(' '+img.getAttribute('title'));I’d appreciate your comments on all this.
Regards,
Paul.
Paul Adler
21 Dec 06 at 7:14 pm
Paul, I don’t have IE handy, but I’d guess that the space is allowing the text to wrap under the image by creating a “word break.” That’s a simple solution to the problem. Simple is good!
Xaprb
21 Dec 06 at 8:43 pm
Can you try putting a space before your title on your demo page? I’d be interested to see if this fixes the problem that I see on your page with both ie 6 and 7, where the padding between the image and the border is not uniform. There are more pixels on the right hand side than on the left. I’d also be interested to see what this does in the browser you are using.
Paul Adler
21 Dec 06 at 9:05 pm
I put up another demo page, and in Firefox 2 on Ubuntu the pages look pixel-identical to me (screenshot). Does it look different in IE?
Xaprb
22 Dec 06 at 8:52 am
Yes, It does look different in IE and it’s definately not pixel identical. It must be how the browser is handling the whitespace. I downloaded the javascript file referenced in your new demo page, and I can’t see where you have concatenated a space to your title. I did it this way: var txt = document.createTextNode(‘ ‘+img.getAttribute(‘title’));
Also for the benefit of other people looking at this site, I used the code from Framboos to handle putting captions under thumbnail images. I found that this caused a formatting problem displaying images that were not part of a link. There was extra space between the title and the caption, probably from the extra whitespace that some browsers add in when you add an element. This was avoided by checking if(parent.tagName == ‘A’) and running Framboos code else running just the IMG code that’s listed here.
if(parent.tagName == 'A'){ grandparent.insertBefore(frame, parent); parent.removeChild(img); grandparent.removeChild(parent); frame.appendChild(parent); parent.appendChild(img); frame.appendChild(txt); } else{ parent.insertBefore(frame, img); parent.removeChild(img); frame.appendChild(img); frame.appendChild(txt); }Hope this helps someone.
Paul Adler
23 Dec 06 at 2:30 am
Ack — I somehow referenced the same JS file (I was sure I referenced the new one). Anyhow that mistake is fixed now.
Xaprb
23 Dec 06 at 8:27 am
Still has the same problems in ie 6 and 7. There is no change. Very strange, because it works fine for me from my code.
Paul Adler
23 Dec 06 at 6:35 pm
I wonder if it has to do with doctype and standards mode.
Xaprb
23 Dec 06 at 6:46 pm
There’s a pre-packaged version of this that works fairly well at http://www.quickcapt.com/
Justin Gehring
22 Mar 07 at 5:58 pm
[...] Descripciones automáticas en nuestra imágenes con javascript no obstrusivo es lo que nos ofrece Xaprb [...]
Propiedad Privada » Blog Archive » De web varia
3 Jul 07 at 5:42 am
Could the script me adapted to work with multiple areas on an image map (using usemap), with each caption appearing at the bottom of the defined
Ian Tresman
1 Oct 08 at 10:10 am
Good stuff! I’ll suggest an addition: Auto numbering of figures with cross references from the text.
I was looking for my suggestion when I stumbled across your article. I googled “css auto number figure” and your page is listed top (well, third but I’m not counting the first 2 links to MS Access related stuff).
Oddly, no more than 5 minutes earlier I wrote an image and caption the old way (exactly as in your document). Since I’m new to CSS/HTML, I just invented it… wishing there was some way to just use the title from the image for the caption.
How lucky for me I found your post. Thanks.
Mark
26 Jan 09 at 3:41 pm
Very cool trick. I will test this one out as soon as I find a good use for it. thanks!
pakistani designer
14 Jan 10 at 8:25 pm
Hi – I just wanted to say that I found this script to be perfect for an easy way to display captioned images on my blog. You can see the result here: http://www.jaygoel.com/blog/laser-cut-enclosure-part-1/
Thanks!
Jay
Jay
12 Apr 11 at 11:09 pm