CSS Drop Shadows
Abstract
This article explains a technique for adding drop shadows to XHTML pages without introducing awkward and semantically challenged mark-up to the page.
The technique can be applied to any XHTML/CSS page, and the JavaScript and CSS presented in here can be easily adapted to local conventions used for marking up images.
The Problem
The page presentation design I had created for my site called for displaying photographs reminiscent of a “real” photographic print. The image needed a white border, and to make the border visible on a white background, it needed something to offset it from the page. Although a simple 1 or 2 pixel line around the white border would be sufficient, it is also very dull and boring.
As of this writing, there are solutions, such as iWeb from Apple and an upcoming version of RapidWeaver from Realmac Software[1], that will add drop shadows to images published. Also, it’s quite possible to use Photoshop to add them as well. However, this is done by modifying the source image and adding a decorative border to it. Keeping within the spirit of XHTML/CSS, I felt it was better to define the image as just the image, and the decorative border as the mark-up – if I change my mind about the page design later, I will not have to re-export all the images, sans borders. It is, in my opinion, the Right Thing To Do.™
Unfortunately, the current CSS specification does not (and, thus, neither do the web browsers I wanted to target: Internet Explorer 6, Firefox, and Safari) have drop shadows for images. My headache was just beginning…
Research
Stumbling around the ‘Net, I came across this article. The author, Jeff Harrell, describes a method for implementing drop shadows with pure CSS. This method involves wrapping the image in a <div> container which holds the image, plus four other <div> containers which place the four corners of the shadow:
<div class="shadow">; <img src="test.jpg" alt="" /></div>
With the appropriate CSS definitions to define the different classes, using absolute positioning for the corner graphics, it appeared to be a robust mechanism (albeit the mark-up is semantically “impure”) to add shadows to any image.
Of course, Harrell’s method presupposes that any XHTML code would include the extra containers, which is impractical to expect from either a website creation tool (such as RapidWeaver or iWeb), or when creating websites by hand-coding.
A solution to this difficulty was proposed by Jon Rogers in his article on a dynamic approach, using JavaScript, to implementing Harrell’s technique. Rogers’ method is to use JavaScript to locate all images with in a source XHTML file, and dynamically modify the page in the user’s browser to wrap the image with the <div> containers that Harrel’s method uses. The advantage of the dynamic code is that the original mark-up remains semantically “pure”, and leaves the rendering of shadows up to the user’s browser, which is within the spirit of XHTML as a mark-up language, not a presentation language.
The Solution
Local Mark-up Conventions
The page creation application I use for this site, timofejew.com, is a product from Realmac Software called RapidWeaver. In order to use Harrell and Rogers’ techniques, an understanding of how RapidWeaver creates each page’s XHTML markup is required[2].
There are four mark-up idioms (that I care about) used by version 3.2.1 of RapidWeaver (the version in use at the time of writing). They are:
Vanilla Image
This mark-up is the basic insertion of an image into a Styled Text, Blog, or Blog Archive page.
<img class="imageStyle" src="test.jpg" alt="Test" />Left Floated Image
If an image in a Styled Text, Blog, and Blog Archive page will be to the left (with text flowing around it), it would produce this mark-up:
<div class="image-left"> <img class="imageStyle" src="test.jpg" alt="Test" /></div>
Right Floated Image
If an image in a Styled Text, Blog, and Blog Archive page will be to the right (with text flowing around it), it would produce this mark-up:
<div class="image-right"> <img class="imageStyle" src="test.jpg" alt="Test" /></div>
Photo Album Thumbnails
The thumbnail navigation page for a Photo Album page has each generated thumbnail wrapped in a table cell using this mark-up:
<div class="thumbnail-frame"> <a href="files/pagen-zzzz-full.html"> <img src="files/pagen-zzzz-thumb.jpg" alt="test.jpg" width="x" height="y" /> </a> Test Image</div>
Vanilla CSS
Typical (or “vanilla”) CSS definitions used to support these four idioms would be:
img { border: none; } .imageStyle { } .image-left { float: left; } .image-right { float: right; } .thumbnail-frame { }
Of course, for a better looking page, padding would need to be added to the imageStyle, image-left, and image-right classes. However, it is up to the theme designer to make these choices – these “vanilla” definitions are simply used for illustration.
As a website typically has many graphical elements, not all elements should automatically have drop shadows applied. I used the following design criteria for applying drop shadows:
- The only images that should have drop shadows applied to are those in the main content area of a page. Any images in the header, footer, sidebar, etc. should not have drop shadows.
- By default, images in the main content area should not have drop shadows.
- If an image in the main content area is tagged as floating left or right, it should have drop shadows.
- All thumbnail images for the Photo Album page should have drop shadows.
- Any image with drop shadows applied should gracefully degrade if the user’s browser does not have JavaScript enabled.
- The XHTML DOCTYPE should be “Strict”, and not “Transitional”.
To support this, the following CSS definitions were created (note that only image-left is defined, as image-right is symmetrical):
img { border: none; } .imageStyle { } .image-left { float: left; margin-right: 10px; border-left: 1px solid #eaeaea; border-right: 1px solid #eaeaeaea; padding: 0 1px; background-colour: #bfbfbf; position: relative; } .image-left .imageStyle { padding: 5px 5px 9px; background: url(images/shadows/bottom.gif) #ffffff repeat-x bottom left; } .image-left .topleft { width: 2px; height: 4px; background: url(images/shadows/tlcorner.gif) no-repeat top left; position: absolute; top: 0; left: -1px; } .image-left .topright { width: 2px; height: 4px; background: url(images/shadows/trcorner.gif) no-repeat top right; position: absolute; top: 0; right: -1px; } .image-left .bottomleft { width: 4px; height: 4px; background: url(images/shadows/blcorner.gif) no-repeat bottom left; position: absolute; bottom: -1px; left: -1px; } .image-left .bottomright { width: 4px; height: 4px; background: url(images/shadows/brcorner.gif) no-repeat bottom right; position: absolute; bottom: -1px; right: -1px; }
Lines 1 and 2 support the design objectives 1-3 (that is, to no automatically add drop shadows to images not explicitly stated to have them).
Lines 4-16 provide:
- The appropriate positional floating for the image (line 5).
- A margin between the final shadowed image and the text that flows around it (line 6). Note that for image-right, it is a margin-left.
- The darkest shadow around the sides (lines 7-10).
- A white border to simulate a “real” photograph (line 14).
- The lightest shadow around the sides (lines 9 and 10).
- The larger shadow for the bottom (lines 9 and 15).
With just these definitions, we have a pretty decent pure CSS drop shadow already. Not to my liking exactly (the corners look a tiny bit wrong). However, we have not executed any JavaScript yet, so design objective 5 (graceful degradation) has been achieved. The shadow looks good, but not great.
In order to improve the look of the shadow, a modified version of Harrell and Rogers’ technique is now applied. With a surprising amount of code. In fact, lines 17 through 48 are all dedicated to simply placing the four corners of the shadow on the image. It’s important to note that since we’re using absolute positioning, the enclosing <div> must be marked as a relative position (line 11).
It’s also important to note that lines 38 and 46 are hacks to get around bugs that all three browsers (IE 6, Firefox, and Safari) have. This will be discussed in the next section.
Drop Shadow JavaScript
Initialization
The code below is the main “hook” to execute the drop shadow code. It is run every time the page is loaded.
window.onload = function() { // if we can't do this, things will go bad if (!document.createElement) return; var an = navigator.appName.toLowerCase(); var is_ie = (an == "microsoft internet explorer"); // patch album thunbs - do this step before addDropShadows() patchAlbumThumbs(); // add drop shadows addDropShadows(is_ie); }
Adding Drop Shadows (Any Image)
The following code will add the four corner <div> containers to a <div> with the class of image-left or image-right.
function addDropShadows(is_ie) { var div = document.getElementsByTagName('div'); for (var i = 0; i < div.length; i++) { if (!(div[i].className == 'image-left' || div[i].className == 'image-right')) continue; // patch margin-bottom for non-IE browsers if (!is_ie) { var img = div[i].getElementsByTagName('img'); for (j = 0; j < img.length; j++) { img[j].style.marginBottom = '-5px'; } } // clone the node var img = div[i].cloneNode(true); // create a replacement div var shadow = document.createElement('div'); shadow.className = img.className; // append all children var kid = img.getElementsByTagName('*'); for (j = 0; j < kid.length; j++) { shadow.appendChild(kid[j]); } // add new divs var topleft = document.createElement('div'); topleft.className = 'topleft'; shadow.appendChild(topleft); var topright = document.createElement('div'); topleft.className = 'topright'; shadow.appendChild(topright); var bottomleft = document.createElement('div'); topleft.className = 'bottomleft'; shadow.appendChild(bottomleft); var bottomright = document.createElement('div'); topleft.className = 'bottomright'; shadow.appendChild(bottomright); // replace the node div[i].parentNode.replaceChild(shadow, div[i]); } }
- The i loop will examine each <div> container that has a class of image-left or image-right in the document, and limit processing of the rest of the function to those elements and their child nodes (lines 2-4, 46).
- Every image needs a bottom margin hack. The hack for IE 6 was set in the CSS definition (-1 pixel), whereas Firefox and Safari require -5 pixels. In an ideal world, the bottom margin should be zero (lines 7-12).
- A working copy of the element is created. This is to avoid IE 6 from having a heart attack (lines 15-19).
- Each child node (and there should be exactly one) is added to element copy. Typically, this will be either an <img> or <a> tag. It’s important to note that for IE 6 to function correctly with an image that has an anchor around it, the anchor must be between the shadow container and the image itself (lines 21-25).
- The four <div> containers for the shadow corners are created and added to the shadow container (lines 27-42).
- Finally, the original image container is replaced with the shadow container, and the page will reflect this immediately (line 45).
Adding Drop Shadows (Thumbnails)
The thumbnails for a Photo Album page are using a table layout with old-fashioned cell alignments. Simply replacing the image with a wrapper isn’t good enough, unfortunately, as the left and right borders of the shadows will stretch to fill the entire cell that contains the image.
In order to overcome this limitation, all of the images are wrapped in a dynamically created single-cell table. Since this is dynamically generated, the mark-up semantics gods will not smite me (I hope).
function patchAlbumThumbs() { var div = document.getElementsByTagName('div'); for (var i = 0; i < div.length; i++) { if (div[i].className != 'thumbnail-frame') continue; var a = div[i].getElementsByTagName('a').item(0); var shadow = document.createElement('div'); shadow.className = 'image-left'; shadow.appendChild(a.cloneNode(true)); var img = shadow.getElementsByTagName('img').item(0); img.className = 'imageStyle'; var wrap = document.createElement('table'); wrap.className = 'thumbnail-wrap'; cell = wrap.insertRow(0).appendChild(document.createElement('td')); cell.appendChild(shadow); a.parentNode.replaceChild(wrap, a); } }
- The i loop will examine each <div> container that has a class of thumbnail-frame in the document, and limit processing of the rest of the function to those elements and their child nodes (lines 2-4, 22).
- A wrapping table, with exactly one row and one column is created (lines 16-18).
- An empty <div> is created, and given a class of image-left (lines 8-9).
- A copy of the first (and only) anchor (including the child thumbnail image) within the thumbnail <div> is found and added to the shadow <div> (lines 6, 11).
- The “naked” <img> tag is given a class name of image-left to get wrapped later by addDropShadows() (lines 13-14).
- The shadow <div> is added as a child of the single cell of the wrapping table (line 19).
- The original anchor is replaced by the new wrapped anchor (line 21)
XHTML Mark-up
Based on the choice to only support Internet Explorer 6, Firefox, and Safari, and combined with the ability (and preference) of RapidWeaver to produce strict XHTML code, the decision was made to only support XHTML documents of type “Strict”. What this means is that every page that uses this technique must have as it’s first line the DOCTYPE declaration of “Strict”:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
As a debugging aid, if the shadows have the bottom corners mis-aligned, chances are that the DOCTYPE declaration is either missing, or is “Transitional”. Check this first before assuming there is a bug with your implementation of this technique. Further to that, RapidWeaver has a “Tidy” and “Optimized” setting for output XHTML generation that will be “helpful” and change the DOCTYPE under your feet if it believes that the XHTML generated for the page is Transitional. The solution to this is to make sure that the output page generation setting is set to “Default”, which will not molest the DOCTYPE header. Of course, with that setting, the onus is on the RapidWeaver user to ensure that the page is indeed XHTML Strict if any custom markup is added to it. The XHTML validator at validate.w3.org is invaluable to spot-check your generated code.
Known Bugs
There is only one known rendering bug, and that is with IE 6. If an image is left aligned, the first line of the wrapped text will be shifted to the right by 2em. Right aligned images have no symmetrical rendering defect. This rendering defect is considered minor, and in my opinion, does not detract from the technique.
Oh, and it’s probably going to horribly break in Opera. But that is such a marginal browser, I’m not too concerned. If anyone does test this technique on Opera, and can supply patches to me, I’d be delighted to include them in a future revision of this article.
There is also a RapidWeaver bug (at least in version 3.2.1). Plug-in pages refer to the JavaScript library as javascript.css rather than the correct javascript.js. One simple fix for this behaviour, until it is corrected in a future release of RapidWeaver, is to make a symbolic link from javascript.css to javascript.js. The text/javascript is correctly set, and the extension is irrelevant – RapidWeaver and the browser will then load the JavaScript library properly if this fix is applied.
Further Enhancements
These are future enhancements that I most likely will never get around to making. Mainly because I’m lazy.
- When a page is being updated by the JavaScript, the images will shift location on the page by the padding amount. This “real-time” rendering isn’t very distracting, but it could be improved. If an image has a supplied width and height, the code could be modified to add the width and height plus the padding to the surrounding <div>. This would not eliminate the shifting, but may reduce it.
- The offsets for the margins are currently defined for XHTML Strict, rather than Transitional (or “quirks”). Querying the document.compatMode DOM attribute can tell us what mode the page is in, and the offsets could be adjusted depending upon whether the browser will render it quirky or not.
- Using other DOM attributes, document.bgColor or element.style.backgroundColor, it should be possible to dynamically create the border and corner images on the fly. I so do not have the patience for coding this.
Conclusions
Implementing drop shadows in CSS and JavaScript is a reasonable stop-gap measure to keep pages semantically “pure” until the appropriate image attributes are added to a subsequent revision of the CSS specification. Although, even if a shadow attribute for images is added, the flexibility of this technique (to add any design of shadow) and the fact that it will take years for all users to upgrade to a browser that does support a future CSS specification, means that the technique presented here should remain a viable method for years to come.
In addition, although there are web design applications coming on to the market that will modify an image and add drop shadows to it before uploading the the web server, the images (especially JPEG images) grow surprisingly in size. By adding in shadows with CSS (even including the 4 corner images), the size is reduced. And since the corner images are cached by the browser, there is no communication overhead after the images are initially fetched.
That, and I want to put a positive spin onto this technique. I did spend a couple of days of effort on it, after all…
Resources
The images for the shadows referred to in this article are available as shadows.tgz on this site.
External Links
- The Shape of Days: My Contribution to the CSS shadow kerfuffle
- hounddog32: Another contribution to CSS Drop shadows
- A List Apart: CSS Drop Shadows
- A List Apart: CSS Drop Shadows II: Fuzzy Shadows
- yDSF – Robust CSS Drop Shadows
- Simon Wilson: CSS Drop Shadows
- 1976design.com: Easy CSS drop shadows
- wg: Fun with Drop Shadows
Copyright and License
The text of this article, including the code and images contained within, is copyright © 2006 Peter Timofejew. All rights reserved.
Permission is granted to modify the code for use, personal or commercial, as long as credit is given in the source code of the derivative work.
This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- August 16, 2006 Update: as of RapidWeaver 3.5, Photo Album pages behave differently (they are now divisions), and the existing code will not add drop shadows properly. I will attempt to update this code, but since I have now switched over to using iWeb, this may not happen (or, rather, I may fix the code as it’s in use by others, but I will most likely not update this document). And yes, I still believe iWeb’s approach is not the Right Thing To DoTM, but the great flexibility to design web pages, at the expense of semantic purity, has won me over. At least for now… [↩]
- August 22, 2008 Update: I’ve abandoned RapidWeaver and iWeb as content management tools, and am now using the opensource WordPress. Although the CSS techniques presented below are still applicable, as WordPress dynamically creates pages using PHP, I would not use the JavaScript component, but would instead perform this function in PHP before the page was served to the browser. [↩]
Leave a Reply