|
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:
After 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%; }
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
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.
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
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
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:
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:
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; }
Turn your javascript off, and notice how the box degrades to the original style.
You may find it helpful to download my three-page test suite which contains all the demos. All the code and images are free for you to use.
|