Creating bulletproof graphic link buttons with CSS

A CSS problem I have been wrestling with lately is how to create a bulletproof shrinkwrapping graphic button. By that I mean an image-based button that will expand and contract to fit the amount of text it contains. It is a very useful technique for CMS-driven sites that allow the client to change the text that is displayed on buttons, as well as for multilingual sites.

A successfull bulletproof image-based button should:

  • Automatically grow horizontally to fit any amount of text
  • Grow horizontally and vertically if text size is increased or if the text wraps to multiple lines
  • Retain its appearance within reasonable limits
  • Be able to have rounded (or other non-square) corners
  • Have no unclickable areas
  • Be readable when images are disabled

Doing this with CSS may sound easy at first, but there are several things to be considered that make it a little tricky. I also ran into several browser related problems before ending up with the solution described in this article.

Before I go on to describe the problems and the solution, I'd like to give you a friendly reminder that this technique should not be used to mindlessly replace real form buttons.

A real form button (normally <input type="submit">) works equally well without CSS and JavaScript. A styled link, however, can only submit a form through the help of some JavaScript. And since You cannot rely on JavaScript being available. Period., you need to take that into account. What I did in the project I came up with this solution for was to use JavaScript to replace the input element with one of these buttons. That way, if JavaScript is not available, the user can click a real button instead.

OK, let's move on. The problems, as always when it comes to CSS, occur in Internet Explorer. The first problem is one that I'm not actually sure whether it is a bug or just a different implementation of the specification. I mentioned the problem in Transparent custom corners and borders, version 2, and it consists of IE having problems figuring out the width of block level elements inside floated elements with no width assigned.

When a floated element has no specified width, the browser should adjust the element's width to fit its content – shrinkwrapping. In the case of a floated link acting as a button, that would come in handy as it will make the link automatically adjust its width depending on the amount of text it contains. Sadly, Internet Explorer will not properly shrinkwrap the following combination of HTML and CSS I use for the Transparent custom corners and borders technique.

  1. <div class="cb">
  2. <div class="bt"><div></div></div>
  3. <div class="i1"><div class="i2"><a href="#" class="i3">A box</a></div></div>
  4. <div class="bb"><div></div></div>
  5. </div>

When div.cb is floated without a specified width, it will shrinkwrap to fit its content. The width depends on the content of div.i1, and that part works fine. The problem is with the and elements, which don't expand to the same width as div.i1. That kind of makes sense, since they are empty siblings of div.i1. A workaround is to give div.cb a width, but I really needed shrinkwrapping buttons, so I had to to come up with something else.

I ran into the second problem while exploring other solutions and experimenting with absolutely positioning the corners. That actually worked, except for a one pixel gap that showed in IE at certain sizes, probably due to a rounding error.

What I ended up with is a link that contains four nested span elements. Here's the markup for a button (nicely indented here for readability):

  1. <a class="button" href="#">
  2. <span>
  3. <span>
  4. <span>
  5. <span>Shrinkwrap me!</span>
  6. </span>
  7. </span>
  8. </span>
  9. </a>

Yes, I know. That is a lot of span elements. It isn't pretty, but I can't think of a better way to achieve this effect until more browsers than Safari support Layering multiple background images. In a production situation I would use a script similar to the one in Transparent custom corners and borders, version 2 to remove the span elements from the markup. The end result is the same to the browser, but the markup is cleaner for us humans who have to edit it.

And of course we need a bit of CSS to turn that markup into a nice-looking button:

  1. .button { /* Top left corner, top edge */
  2. float:left;
  3. color:#ddd; /* Text colour */
  4. background:#333 url(button.gif) no-repeat; /* Fallback bg colour for images off */
  5. font:1.2em/1.0 Georgia,serif;
  6. text-decoration:none;
  7. }
  8. .button * {display:block;}
  9. .button span { /* Top right corner */
  10. padding:6px 0 0;
  11. background:url(corners.gif) no-repeat right top;
  12. }
  13. .button span span { /* Bottom left corner, left and bottom edges */
  14. padding:0 0 0 6px;
  15. background:url(button.gif) no-repeat left bottom;
  16. }
  17. .button span span span { /* Bottom right corner */
  18. padding:0 0 6px;
  19. background:url(corners.gif) no-repeat right bottom;
  20. }
  21. .button span span span span { /* Right edge */
  22. padding:3px 12px 3px 6px; /* Extra padding (3px vertical, 6px horizontal) added to give the text some breathing room */
  23. background:url(button.gif) no-repeat right center;
  24. }
  25. .button:hover,
  26. .button:focus,
  27. .button:active { /* Help keyboard users */
  28. outline:2px solid #ff0; /* Not supported by IE/Win :-( */
  29. color:#fff;
  30. }

You'll need to adjust the paddings to fit your image if it has wider or narrower corners and edges.

Oh, I made a demo page that contains a few buttons.

Ideally, this kind of button would be accomplished by using a single background image, so that was my initial goal. I did not manage to reach that goal, however, but had to resort to using two images. The two images are button.gif and corners.gif. If you take a look at them, you will notice that they are much larger than any button should become since they define the maximum size of the button. If you need even larger buttons, just increase the size of the images.

Note that corners.gif is transparent except for in the corners. That way it can be used to put each corner in place without covering any of the other corners.

And that's it. A bulletproof, resizable, shrinkwrapping graphic button.

There are a few caveats though:

  • The corners can't be transparent
  • It may be tricky to give the button a non-flat background colour
  • It only expands as far as the size of your image

To me, those are completely acceptable drawbacks in most situations.

So, does anyone see how this could be further improved?

Update (2007-05-23): The class attributes on the span elements are gone. I was sure I had a reason for using classes instead of descendant selectors at the time, but I can't see the need for them now. So no more classitis, and a little leaner markup.

Posted on May 22, 2007 in CSS