Rounded Corners with CSS and JavaScript

Tweet

Rounded corners are one of the most frequently requested CSS techniques. As with many things in CSS, there are various ways in which this problem can be approached. In this article, I’ll look at the pros and cons of some common techniques and introduce a new technique that utilises both CSS and JavaScript.

Before we dive in to the CSS, let’s remind ourselves of the old fashioned approach to this problem, which uses layout tables:

<table width="200" cellpadding="0" cellspacing="0"> 
<tr> 
  <td width="15"><img src="tl.gif" alt="" /></td> 
  <td bgcolor="#1b5151"></td> 
  <td width="15"><img src="tr.gif" alt="" /></td> 
</tr> 
<tr bgcolor="#1b5151"> 
  <td>&nbsp;</td> 
  <td>Content goes here</td> 
  <td>&nbsp;</td> 
</tr> 
<tr bgcolor="#1b5151"> 
  <td><img src="bl.gif" alt="" /></td> 
  <td></td> 
  <td><img src="br.gif" alt="" /></td> 
</tr> 
</table>

A few years ago this would have been an acceptable solution. Today, it’s an ugly hack: that’s an awful lot of redundant markup for a relatively unimportant visual decoration. In fact, the above code won’t even function as intended in documents served using a strict doctype — small gaps will appear beneath the corner images, caused by the fact that images are inline elements and, hence, leave space beneath the image for the "tails" on letters such as ‘y’ and ‘j’. The solution, as explained by Eric Meyer in Images, Tables and Mysterious Gaps, is to add the following rule to your stylesheet:

td img { display: block; }

This produces the desired result, as shown here.

But, now we’re using CSS hacks to fix ugly table hacks! Let’s look at ways to implement the same effect using only CSS.

As a general rule, any decorative image should be implemented as a CSS background image on an existing page element, rather than being dropped in to the page proper using an <img> tag. It’s easy to determine whether an image is decorative or contains actual content: ask yourself if the absence of the image would have any effect on the overall content of the page. In the case of rounded corners, the answer is obviously not.

CSS background images are remarkably powerful things. You need only look at the many wonderful designs on display at the CSS Zen Garden for evidence of that. Using CSS, a background image can be applied to any element on a page. Furthermore, it can be repeated horizontally, vertically or not at all; it can be positioned within the background area of the image using absolute measurements, or relative to one of the four corners; it can even be made to stay fixed in place when the element’s content scrolls. Unfortunately, CSS 2 imposes one small but significant limitation: you can only apply a single background image to each element on the page. To properly render rounded corners on a <div> we need to apply four background images, one in each corner.

Fixed Width Boxes

If the width of the box to which we’re applying decorative corners is fixed, half of the problem is solved already. If we know that the box will always be 200 pixels wide, instead of creating four background images (one for each corner), we can create two: one for the top of the box and one for the bottom. The challenge is now reduced to applying two background images to our <div>. It’s time to take advantage of our markup.

A box with rounded corners wouldn’t be much fun if it didn’t contain any content. Consider the following:

<div class="rounded"> 
<h3>Exciting features!</h3> 
<p>Your new Widget2000 will...</p> 
<ul> 
 <li>... clean your shoes</li> 
 <li>... walk your dog</li> 
 <li>... and balance your cheque book!</li> 
</ul> 
</div>

Pretty simple, huh? The title of the box lives in an <h3> (I’m assuming <h1> and <h2> have already been used further up the page’s hierarchy) and the content that follows is a paragraph and an unordered list. The key to solving our two background problem lies in the <h3>, which comes right at the top of the box. All we have to do is to apply a background image to the top of the <h3>, and another to the bottom of the containing <div>, and the effect is complete:

div.rounded { 
  width: 200px; 
  background: #1b5151 url(200pxbottom.gif) no-repeat bottom center; 
  padding-bottom: 15px; 
} 
div.rounded h3 { 
  padding-top: 15px; 
  background: transparent url(200pxtop.gif) no-repeat top center; 
}

Click here to see the results.

Well-structured documents are usually full of hooks like this that can be carefully exploited to apply multiple backgrounds and achieve specific visual effects. Learning to identify them is an important part of working with CSS.

Nested Elements

Applying four backgrounds to a single div is still out of our reach. But what if we nested four divs, one for each background? Doing so solves our problem, but comes at the expense of additional markup with no structural value:

<div class="rounded"><div><div><div> 
Content goes here 
</div></div></div></div>

And, in the CSS:

div.rounded { 
  width: 200px; 
  background: #1b5151 url(tr.gif) no-repeat top right; 
} 
div.rounded div { 
  background: transparent url(tl.gif) no-repeat top left; 
} 
div.rounded div div { 
  background: transparent url(br.gif) no-repeat bottom right; 
} 
div.rounded div div div { 
  background: transparent url(bl.gif) no-repeat bottom left; 
  padding: 15px; 
}

The code displays as shown here.

It should be clear what’s going on here. Each of the four divs is assigned a rounded corner background image, positioned in the top-right, top-left, bottom-right and bottom-left respectively. While the width of the containing div is set to 200px, it could just as easily be set to something more flexible for use with liquid designs — the corners would still work, no matter how large or small the containing div was.

We now have a solution to the problem, which uses far less markup than the original tables example. But, it’s still not perfect: it uses three extra divs, which add nothing of value to the overall document structure. Can we do any better? It’s time to look to JavaScript.

Using the DOM

Using JavaScript and the DOM, it’s possible to manipulate the structure of a document after it has been loaded by the browser. Rounded corners are a presentational effect that can be hidden from non-JavaScript user agents without any significant reduction in their overall experience of the site, so there are no ethical problems with using JavaScript for this kind of transformation. Our final solution will require only a single <div> in the source document. We will use JavaScript to dynamically append the three extraneous divs needed for the rounded corner effect.

Here’s the markup:

<div class="rounded"> 
Content goes here. 
</div>

I think you’ll agree that there’s not much we can do to make it simpler than that, except maybe exchange the <div> for a <p> if the content is structurally better defined as a paragraph. Making this switch is left as an exercise for the reader.

Now here’s the JavaScript:

function roundedCorners() { 
  var divs = document.getElementsByTagName('div'); 
  var rounded_divs = []; 
  /* First locate all divs with 'rounded' in their class attribute */ 
  for (var i = 0; i < divs.length; i++) { 
    if (/broundedb/.exec(divs[i].className)) { 
      rounded_divs[rounded_divs.length] = divs[i]; 
    } 
  } 
  /* Now add additional divs to each of the divs we have found */ 
  for (var i = 0; i < rounded_divs.length; i++) { 
    var original = rounded_divs[i]; 
    /* Make it the inner div of the four */ 
    original.className = original.className.replace('rounded', ''); 
    /* Now create the outer-most div */ 
    var tr = document.createElement('div'); 
    tr.className = 'rounded2'; 
    /* Swap out the original (we'll put it back later) */ 
    original.parentNode.replaceChild(tr, original); 
    /* Create the two other inner nodes */ 
    var tl = document.createElement('div'); 
    var br = document.createElement('div'); 
    /* Now glue the nodes back in to the document */ 
    tr.appendChild(tl); 
    tl.appendChild(br); 
    br.appendChild(original); 
  } 
} 
/* Run the function once the page has loaded: */ 
 
window.onload = roundedCorners;

The script is divided in to two logical sections. The first section iterates over all of the <div> elements in the document, building an array of those that contain 'rounded' in their class attribute (remember, elements can have multiple classes separated by spaces). The second part of the script goes through each of these elements in turn, creating three additional divs and wrapping them around the original. Let’s look at the code for that in more detail:

original.className = original.className.replace('rounded', '');

Here we remove the class "rounded" entirely from our original <div>. The reason for this will become clear in the CSS; essentially, we don’t want the original styles applied to affect that element any more.

var tr = document.createElement('div'); 
tr.className = 'rounded2';

We have created out outer-most <div>, which will be used to apply the top-right background image as well as the overall width of the box. Note that we have set the class to 'rounded2'; this will be defined in our CSS, with subtle differences from the 'rounded' class provided to non-JavaScript-enabled clients.

/* Swap out the original (we'll put it back later) */ 
original.parentNode.replaceChild(tr, original);

The W3C DOM does not provide a direct method to replace a node in a document with another node. Instead, you must use the replaceChild() method of a node to replace one of its children with another node. A useful trick to replace the node you’re looking at is to access its own parent using the parentNode property, then use /#c#.replaceChild to swap it for something else. If that doesn’t make sense to you, don’t worry — just think of the above line as replacing our original node with the new tr node we have just created.

/* Create the two other inner nodes */ 
var tl = document.createElement('div'); 
var br = document.createElement('div'); 
/* Now glue the nodes back in to the document */ 
tr.appendChild(tl); 
tl.appendChild(br);

We’ve now created three new <div> elements and inserted them in to the document. All that’s left is to re-insert our original node, complete with its contents:

br.appendChild(original);

At this point, our actual document tree is almost identical to that in the four nested <div> example above, the only difference being that the outer element has a class of 'rounded2' instead of 'rounded'. Here’s the CSS:

div.rounded { 
  width: 170px; 
  padding: 15px; 
  background: #1b5151; 
} 
 
div.rounded2 { 
  width: 200px; 
  background: #1b5151 url(tr.gif) no-repeat top right; 
} 
div.rounded2 div { 
  background: transparent url(tl.gif) no-repeat top left; 
} 
div.rounded2 div div { 
  background: transparent url(br.gif) no-repeat bottom right; 
} 
div.rounded2 div div div { 
  background: transparent url(bl.gif) no-repeat bottom left; 
  padding: 15px; 
}

Here’s the result.

The first set of rules, for div.rounded, is only used in browsers that do not execute the JavaScript. Note that the width is 170px and the padding 15px, which adds up to a total width of 200px (the width plus the left and right padding). If you need this to work in IE 5/Windows, which interprets padding values differently, you’ll need to apply the infamous box model hack. You’ve already seen the second set of rules in the previous example.

Looking Ahead

The above technique will work in all modern browsers, and all future browsers that support the CSS2 and DOM 2 standards. CSS 3 introduces a number of new ways to achieve this effect, which will render the above techniques obsolete. As well as native rounded corner support (already available in the Mozilla family of browsers) CSS features the powerful ::outside pseudo-element, which allows additional stylable elements to be inserted in a manner similar to the JavaScript example shown in this article. If that’s not enough, border images will allow pretty much any border decoration you could care to think of.

Unfortunately, it will be years before CSS 3 support is widely available. Until then, JavaScript is more than capable of taking up some of the slack.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.facebook.com/profile.php?id=1559878422 Viktor Kultsten

    Thank you for the tutorial. I found out a bug in the last JavaScript code: You forgot to close the first comment which caused to escape a part of the code. It failed the whole thing. Escape the comment or delete it and it will work.

    • http://onsman.com/ ronsman

      Good catch, Viktor. Should be fixed now.