Webforumz Newsletter - October 2007

Tutorials

CSS boxes with all the glamour

What you get: fully flexible CSS-based boxes with drop shadows, a gradient background, and tiny image sizes. The method can be extended to yield rounded corners too, but I don't cover that here (you need absolute positioning for that).

Limitation: they can't be used against a patterned background (must be flat colour).

The makeover shots

Before glamour:

Before CSS Glamour

After glamour:

After CSS Glamour

The basic set-up

I like using a <dl> whenever the box has a title; the pairing of <dt> and <dd> makes a semantic connection between title and content. You could use a heading instead, such as <h4>, but I don't think these boxes should pollute the heading structure. In books, an expanded table of contents will list all headings, but it won't count these little boxes. For cases where your box title is important enough to be part of the heading structure, use a heading and put the whole thing inside a <div>. The method still works fine, but you will need to adapt it slightly. Anyway, here's the HTML we'll be working with for this tutorial:


<dl class="didYouKnow">
<dt>Did you know?</dt>
<dd>
<p>Four colours are enough to draw a map so that no countries 
sharing a border have the same colour.</p>

<p>This is true no matter how complex the map (so long as it's_
 two-dimensional). 
The proof requires a computer.</p>
</dd>
</dl>
        

We can make a basic gradient box without much effort. This only needs a gradient fill image, one pixel wide. Note that the gradient is a fixed height: it won't stretch to fill the box. This isn't a problem, but you'll probably get the best results by making your gradient about the same height as your typical box.


 dl.didYouKnow {
    width: 30em;
    margin: 2em 4em;
    background: #fff url(gradient.png) repeat-x;
    border: 1px solid #000;
    padding: 1em;
}
dl.didYouKnow dt {
    font-size: 120%;
}
        

See the result of this basic CSS. The background colour must match the bottom of the gradient. You can also align the gradient to the bottom of the box, in which case the background colour must match the top of the gradient.

Glamming it up

For this effect, we need eight more images: four sides and four corners. We will apply these images as CSS backgrounds. Unfortunately, CSS 2.1 only allows you to apply one background image per element. CSS 3 removes this limit, but few browsers support the feature (Safari is one). With IE6 still the most popular browser, we can expect to wait years, not months, for good CSS 3 support.

To overcome this, we just need to add more elements. Since they will be purely presentational, we should use <div>s. In theory, eight should be sufficient. In practice, I've found it's best to remove the gradient background from the <dl> and use a ninth <div> instead. I don't know why, but leaving the gradient on the <dl> has led to buggy behaviour for me, and therefore I cannot recommend it.


<div class="didYouKnow"><div><div><div><div><div><div><div><div>
<dl>
<dt>Did you know?</dt>
<dd>
<p>Four colours are enough to draw a map so that no countries 
sharing a border have the same colour.</p>

<p>This is true no matter how complex the map (so long as it's_
 two-dimensional). 
The proof requires a computer.</p>
</dd>
</dl>
</div></div></div></div></div></div></div></div></div>
        

Markup purists will reject this, because it sullies their pages with lots of "unnecessary" <div>s. I address the issue of extra markup later in this article, and I also explain how to generate this markup with javascript (keeping your source files clean). Before you reject my method, at least read my argument. Perhaps this extra markup is not as bad as you think.

Making the images

It helps to be methodical. I used Photoshop, but any half-decent paint program will do. First, create a medium-size image (say, 300*200 pixels). Set the background colour to match your page background. Make a box shape (work in vector graphics, not raster) and add a drop shadow; play with the parameters until you find something you like.

A Box Shape

A Box Shape

Next, resize the box so that it's nearly as small as you can get without changing the shadow in the middle of each side. Crop the image as closely as you can. The pixels on the edges must match your web page background colour exactly.

Make the box smaller, without damaging the drop shadow.

Make the box smaller, without damaging the drop shadow.

Starting in the middle of each side, select a one-pixel wide (or high) slice of the drop shadow, crop, and save as: top.png, left.png, right.png, and bottom.png. Each time you'll need to undo the cropping afterwards. Make sure your selection touches but does not include the edge of the square and goes all the way out to the edge of the image. It's helpful to zoom in.

Selecting a side

Selecting a side

Now select the corners. Make sure you create a large enough selection so that the entire blending of the corner shadow is included. Again, your selection must go all the way to the edges of the image. Save these as: cornerTL.png, cornerTR.png, cornerBL.png, and cornerBR.png (where T stands for top, B for bottom, L for left, R for right).

Selecting a corner

Selecting a corner

Your images should be tiny in file size, but you can make them even smaller using pngCrush, which will compress PNGs without any quality loss. PngCrush can also remove the gamma information from your PNGs: you should do this, because otherwise the colours will look wrong in IE.

If you don't want to mess around with pngCrush, then just save them as GIFs instead (yuck).

Styling the box

First some basics:


div,dl,dt,dd {		/* Global reset */
    padding: 0;
    margin: 0;
}
div.didYouKnow {
    width: 30em;
    margin: 2em 4em;
}
div.didYouKnow dt {
    font-size: 120%;
}
div.didYouKnow dd {
    margin: 0 2em;
}
            

Now let's add the side images. These should be applied first, so that the corners will stack above them:


div.didYouKnow {
    background: url(top.png) repeat-x;
}
div.didYouKnow div {
    background: url(right.png) right repeat-y;
}
div.didYouKnow div div {
    background: url(left.png) repeat-y;
}
div.didYouKnow div div div {
    background: url(bottom.png) bottom repeat-x;
}
			

Now your box should look like this:

examp

Now let's add the corners:


div.didYouKnow div div div div {
    background: url(cornerTL.png) no-repeat;
}
div.didYouKnow div div div div div {
    background: url(cornerTR.png) top right no-repeat;
}
div.didYouKnow div div div div div div {
    background: url(cornerBL.png) bottom left no-repeat;
}
div.didYouKnow div div div div div div div {
    background: url(cornerBR.png) bottom right no-repeat;
}
         

Your box should now look like this:

Add the gradient background on top of everything else:


div.didYouKnow div div div div div div div div {
    background: #fff url(gradient.png) repeat-x;
}

Oh dear! The drop shadow has vanished:

The gradient is obscuring too much. To fix this, we need to add exactly the right amount of padding. The padding should match the size of each of your side images. In the example files, these are:

  • top.png = 15px
  • right.png = 25px
  • bottom.png = 27px
  • left.png = 16px

We add this padding to the <div> immediately outside the gradient <div>. We'll also add some padding to the gradient <div>; strangely, it seems you must set some padding or a border on this element, or the effect will break. I don't understand why. If you want zero padding, I've found two solutions: set padding to 0.05em, or add a transparent bottom border.


div.didYouKnow div div div div div div div {
	background: url(cornerBR.png) bottom right no-repeat;
	padding: 15px 25px 27px 16px;
}
div.didYouKnow div div div div div div div div {
	background: #fff url(gradient.png) repeat-x;
	padding: 0.5em;
}

The box is now complete:

You can see this box on my second demo page. Here's the full CSS:


div,dl,dt,dd {		/* Global reset */
	padding: 0;
	margin: 0;
}
div.didYouKnow dt {
	font-size: 120%;
}
div.didYouKnow dd {
	margin: 0 2em;
}
div.didYouKnow {
	width: 30em;
	margin: 2em 4em;
	background: url(top.png) repeat-x;
}
div.didYouKnow div {
	background: url(right.png) right repeat-y;
}
div.didYouKnow div div {
	background: url(left.png) repeat-y;
}
div.didYouKnow div div div {
	background: url(bottom.png) bottom repeat-x;
}
div.didYouKnow div div div div {
	background: url(cornerTL.png) no-repeat;
}
div.didYouKnow div div div div div {
	background: url(cornerTR.png) top right no-repeat;
}
div.didYouKnow div div div div div div {
	background: url(cornerBL.png) bottom left no-repeat;
}
div.didYouKnow div div div div div div div {
	background: url(cornerBR.png) bottom right no-repeat;
	padding: 15px 25px 27px 16px;
}
div.didYouKnow div div div div div div div div {
	background: #fff url(gradient.png) repeat-x;
	padding: 0.5em;
}

The problem of extra markup

It's a shame we need those extra <div>s. With foresight, however, we can limit the real damage that extra markup does to a website. So that we may focus on the relevant problem, however, I should like to dismiss some concerns that may safely be ignored:

1. It increases file size.
2. It makes the code harder to read.
3. It slows down browser rendering.
4. It makes the code less semantic.
5. Google rewards light code with higher rankings, and punishes bloated code.
6. The Google spider will fail to index your content properly, because the extra code will make it get lost, or give up.
7. Lighter code means better keyword density, so better rankings.
8. It offends your aesthetic sense.

(1), (2), and (3) are true, but the difference in file size, legibility, and rendering speed is so slight as to be irrelevant.

(4) is deductively false, because <div>s are semantically neutral.

I think (5) and (6) are superstitions; like the existence of ghosts, their only evidence is anecdotal. Since Google cares about content, not elegant code, it seems most unlikely that it should penalise extra markup. (7) is a category error, because keyword density is about how often your keywords appear in your text, not in your source code (why would Google index the <div>s? They are not human-readable content).

(8) is a valid point. It offends my aesthetic sense too. Then I get over myself and remember that web design is not fine art, and no-one else cares about the elegance of my code.

One objection, however, is indisputably well-founded: 9. Adding extra markup makes design changes more difficult.

Suppose you change your mind about the style of these boxes, and need to edit the markup. You could easily have hundreds of pages to edit. That's a lot of work. I know two ways to prevent this problem:

  • Comment your extra markup, so that you can safely use a find-and-replace function to replace it automatically (in a batch); or
  • Use javascript to generate the extra markup.

Of course, this only applies for repeated use of the effect on medium to large sites. You needn't take such precautions if your website has only five pages.

If you use comments, then using more comments will be more flexible. Put comments in all places that you might want to add or remove <div>s:


<!-- Begin dl.didYouKnow -->
<div class="didYouKnow"><div><div>_
<div><div><div><div><div><div>
<dl>
<!-- Begin inside dl.didYouKnow -->
<dt>Did you know?</dt>
<dd>
<p>Four colours are enough to draw a map so that no countries 
sharing a border have the same colour.</p>

<p>This is true no matter how complex the map (so long as it's_
 two-dimensional). 
The proof requires a computer.</p>
</dd>
<!-- End inside dl.didYouKnow -->
</dl>
</div></div></div></div></div>_
</div></div></div></div><!-- End dl.didYouKnow -->

Javascript genesis

I prefer the javascript solution, because it's completely flexible and requires less effort to make changes. As a bonus, it keeps my source code free of extra markup. Finally, if you worry about SEO (irrationally, I think), bear in mind that the search engine spiders should read the source code without the javascript transformation, and therefore without the extra markup.

Some will argue that javascript is supposed to be used for behaviour, not presentation. I couldn't care less. CSS and javascript are tools, not religious artefacts. Use the best tool for the job.

If you use javascript to alter your markup, be careful that the resulting code remains valid. This is an easy mistake to make: it caught me out! The W3C validator will not detect invalid code that was generated by javascript, because it receives the code without the javascript transformation. You need to submit the generated source code instead. You need only do this for one page, as a test.

The script is simple. You need to run the function generateMarkup() when the page loads. I'll go through it one step at a time. First, we find every <dl class="didYouKnow">:


function generateMarkup()
{
var x=document.getElementsByTagName("dl")
for (i=0;i<x.length;i++)
{
	if (x[i].className) { if (x[i].className=="didYouKnow")
	{

(If you use multiple classes on some of these <dl>s, then you will need more sophisticated className detection. The above method would miss <dl class="funny didYouKnow", for example.) Now we clone the <dl>, and create nine <div>s:

 
var newDL=x[i].cloneNode(true) 
var d1 = document.createElement('div') 
var d2 = document.createElement('div') 
var d3 = document.createElement('div') 
var d4 = document.createElement('div') 
var d5 = document.createElement('div') 
var d6 = document.createElement('div') 
var d7 = document.createElement('div') 
var d8 = document.createElement('div') 
var d9 = document.createElement('div') 

These new elements are not yet attached to the document; they are floating in space. We replace the existing <dl> with the first new <div>:

x[i].parentNode.replaceChild(d1, x[i])

Then we add all the other <div>s, nested inside each other like Russian dolls:


d1.appendChild(d2) 
d2.appendChild(d3) 
d3.appendChild(d4) 
d4.appendChild(d5) 
d5.appendChild(d6) 
d6.appendChild(d7) 
d7.appendChild(d8) 
d8.appendChild(d9) 

Finally, we put the cloned <dl> inside the deepest <div>, and move the "didYouKnow" class name to the outermost <div>:


d9.appendChild(newDL) 
d1.className=x[i].className 
x[i].className="" 

Here's the complete script:


function generateMarkup()
{
var x=document.getElementsByTagName("dl")
for (i=0;i<x.length;i++)
{
	if (x[i].className) { if (x[i].className=="didYouKnow")
	{
		var newDL=x[i].cloneNode(true)
		var d1 = document.createElement('div')
	   	var d2 = document.createElement('div')
		var d3 = document.createElement('div')
		var d4 = document.createElement('div')
		var d5 = document.createElement('div')
		var d6 = document.createElement('div')
		var d7 = document.createElement('div')
		var d8 = document.createElement('div')
		var d9 = document.createElement('div')
		x[i].parentNode.replaceChild(d1, x[i])
		d1.appendChild(d2)
		d2.appendChild(d3)
		d3.appendChild(d4)
		d4.appendChild(d5)
		d5.appendChild(d6)
		d6.appendChild(d7)
		d7.appendChild(d8)
		d8.appendChild(d9)
		d9.appendChild(newDL)
		d1.className=x[i].className
		x[i].className=""
	}}
}}

We also need to add some fall-back styles, in case javascript is disabled. You can use exactly the same CSS as for the original basic example. My juggling of class names ensures that there will be no conflict between the styles:


dl.didYouKnow {
	width: 30em;
	margin: 2em 4em;
	background: #fff url(gradient.png) repeat-x;
	border: 1px solid #000;
	padding: 1em;
}
dl.didYouKnow dt {
	font-size: 120%;
}
dl.didYouKnow dd {
	padding: 0;
	margin: 0 2em;
}

You can see the final result on third demo page. Turn javascript off, and notice how the box degrades to the original style.

You may find it helpful to download my three-page test suite. All the code and images are free for you to use.