The CSS We Grew Up With

For those of us who learned and worked with CSS before the technology offered reliable support for the large (and, thanks to the tireless efforts of many, continuously growing) body of features that is with imprecision referred to as CSS3 — we may recall a styling language that was quirky, overtly limited, and tedious to use. It’s not that it was ever actually bad. Its most fundamental principles and mechanics (namely, the way style properties cascade) have been around since the mid-nineties, and they are elegant and powerful when taken advantage of, but sometime in the mid-aughts we started using CSS to develop large and complex web applications and support rich user interaction, and — like pretty much all of the core technologies embedded in modern web browsers—it wasn’t really designed for that from the ground up.

I suspect that around the time that websites went off the reservation by shifting from being simple structured and hyperlinked documents to more sophisticated applications with demanding user interface requirements, CSS’s inadequacies in that realm were repellent to a lot of good programmers who wanted something with better facilities for modularity and scalability, and way less primitive support for visual effects and layout. At the same time, out of the basic crappiness of CSS and the need to still use it (because the websites had to get built and it’s not like there was a better tool for that, except for tables maybe lol), another group of programmers (and would-be programmers) emerged. If you were a front-end web developer circa 2007, to be anywhere near effective, you had to understand CSS intimately enough to know its constraints, be crafty enough to be able to bend them, and wise enough to know when the tradeoff of a cumbersome technique was or wasn’t worth it. A driven and engaged community was borne by this climate, and uncovered an arsenal of cutting-edge techniques that could be used to solve common problems. These problems included:

  • Having elements with fluid dimensions that need to render rounded corners and/or drop-shadows.
  • Vertically centering an element in a container of an arbitrary height.
  • Vertically aligning a series of block-like elements, each of which have unknown heights.
  • Displaying a footer that rests along the bottom of the viewport if the page content is shorter than the viewport, but flows naturally after the content when it does extend past the height of the viewport. (This is a “sticky footer”).
  • Producing a layout consisting of a header, a fluid primary column flanked by two fixed-width secondary columns (one on the left, one on the right) which come after the primary column in the markup, and a footer. (This is the “Holy Grail Layout”).

To address these sorts of requirements, for myself, learning the necessary tricks— many of which are obsolete because standard CSS today has replaced them with much simpler and more robust and optimal solutions, was the gateway drug into web development across the entire stack. In this post, I revisit one of the most well-known ones, which has been deprecated for years now. Douglas Bowman’s legendary 2003 article about this is still up on A List Apart. To illustrate the technique, I use only selectors and properties that would’ve been reliably available before CSS3, but, for clarity’s sake, I omit hacks that were necessary to address browser-specific bugs.

Sliding Doors

Before CSS had direct support for rounded corners and drop-shadows (with border-radius and box-shadow, respectively), it was only possible to achieve these visual flourishes by using background images. If you were dealing with a single element that had a specific width and height (maybe a button, for example), you could just embed whatever effect(s) you needed in an image and apply it to the element using the background-image property (before CSS3, only a single image could be assigned per element), and call it a day. Since a single background image was constrained to a fixed size, things became more interesting if the element’s dimensions were supposed to change depending on the size of its contents. Let’s imagine a standard tab pane, where the individual tabs have rounded corners, inset drop-shadows, and can be different widths depending on what their labels happen to be. To make the example more realistic, we’ll also account for both inactive and active tabs.

A tab pane where the tabs have rounded corners, inset drop-shadows, and varying widths.

The underlying method behind applying background images to fluidly sized elements is to slice the image up into a minimum viable number of separate parts, and apply each part to a different element in a way that allows the images to be layered and ultimately appear as one unit. For the tabs, each tab can consist of one background image that contains just the left side corner of the tab, and another that contains the right side corner, and also everything in the space between the left and right corners. We’ll need a set of these images for both the inactive and active tab.

Background images for the left and right components of an inactive and active tab.

For the right side of the tab, we can’t escape having to give the image a finite width, but we can compromise by giving it a width that is wide enough that it’ll account for any reasonable size of a tab label that we’d anticipate (our ideal goal is to create a tab system that allows for tabs to be any width, but we recognize that, in practical reality, it’s not like we’re ever going to have tabs that are 1000 pixels wide). We can also make the tab images extra tall to allow the tab to be as tall as its content dictates (if we change the font-size of the tab labels, for example, the tab would naturally become taller and we wouldn’t want our background images to not accommodate reasonable height fluctuation). We’ll apply the left corner image so that it’s anchored to the top left, on top of the right side image (thereby covering up its left-most part). This is referred to as a “sliding door” because the left image slides (and closes) over the right image. Another compromise we’re making is that the pixels outside of our rounded corner in the left corner image cannot be transparent. If they were transparent, then the right image would be visible through the left corner and the sliding-door effect wouldn’t work. In this example, we assume the background color behind the tabs is white, and we bake that into the background of the images. If our tab pane needed to support multiple page backgrounds, we would have to write more CSS and have more images in order to provide variants for each background. Because each image requires the browser to open an HTTP request to the server to load it, background images slow down the rendering process and increase burden on the server (you could use sprites to optimize the number of distinct images, but that costs some additional complexity and setup and maintenance overhead). When Zeke (one of my business partners/friends) was a design intern at Google in 2009, the engineering team informed him that he couldn’t use rounded corners in his designs due to the amount of additional load it would put on their servers when serving the images to millions of users.


We can markup the tabs as a list of links. The “state-active” class will be used to indicate the active tab. Composing each individual tab of two elements (an li and an a) is crucial because we will need two hooks to apply the background images for each tab. (If we had three hooks, we could approach this differently by slicing three images per tab; the left corner, right corner, and a middle image that could be horizontally tiled — but that would cost more bloated markup and more images for the browser to load).

<ul class='tabs'>
<li><a href=''>Home</a></li>
<li class='state-active'><a href=''>Friend Requests</a></li>
<li><a href=''>Messages</a></li>
<li><a href=''>Notifications (2)</a></li>

We start by styling the tabs container. We give it a font-size that will cascade down to its children, a bottom border, and get rid of its default list-style (assuming it hasn’t been reset already by more general rules in our stylesheet). We’re going to float the list items, so we also need to ensure that it will be at least as tall as its floated constituents (since floating an element removes it from the normal flow) — for this we can apply an old-school clearfix.

.tabs {
font-size: 14px;
border-bottom: 1px solid #000;
list-style: none;
.tabs:after {
content: '.';
display: block;
clear: both;
visibility: hidden;
height: 0;

Next, we style the individual tabs, styling them to be inactive by default. Since the li is the outer constituent of the tab, we’ll make it float: left; and give it a left margin to space the tabs apart horizontally. We’ll also relatively position each tab (we’ll see why we need this later). Since the a is the inner constituent and we want that to make up the entire clickable hit-area of the tab, we’ll make it a block element (necessary because a’s are inline by default), give it some padding, and normalize some default browser styles for links (again, assuming no reset). We’ll apply the right image as a background-image to the li, anchor it to the top right of the element, and the left corner image to the a, anchored to its top left and set to not tile.

.tabs li {
float: left;
margin: 0 0 0 15px;
position: relative;
background-image: url('tab_right.jpg');
background-position: top right;
.tabs li a {
display: block;
padding: 10px 15px;
background-image: url('tab_left.jpg');
background-position: top left;
background-repeat: no-repeat;
text-decoration: none;
color: inherit;

To style the active tab, we just override its background images with alternate versions.

.tabs li.state-active {
background-image: url('tab_right_ACTIVE.jpg');
.tabs li.state-active a {
background-image: url('tab_left_ACTIVE.jpg');

The active tab also needs to cover the bottom black border of the tabs container. Assuming that the foreground of our tabs pane is white, we can do this by absolutely positioning a pseudo-element just under the active tab, right on top of the border. Since we gave the tab list items position: relative;, the pseudo-element’s offset properties are with respect to its parent tab li.

.tabs li.state-active:after {
content: '';
position: absolute;
bottom: -1px;
height: 1px;
left: 1px;
right: 1px;
background-color: #fff;

See a live demo here.