Top 5 CSS Mistakes

This is not a post about the top 10 mistakes that developers make when they write CSS. That’s boring.

The CSS spec itself is littered with mistakes. Deep, fundamentally misguided errors. Real head-slapping WTFs that make you wonder if the people writing the spec had ever created a website in their lives. I’m not exactly following my own advice by complaining, but I think of these more as the “constructive criticism” sort of complaints.

I love making web pages. And, without being immodest, I’m pretty good at CSS. Not the best in the world, but certainly very familiar with it. I do enjoy what CSS and semantic HTML lets you do with a web page. But I am repeatedly annoyed by the language itself.

The saving grace, if there is one to be found here, is that browser makers are getting better and better at conforming to the spec with each version. Even IE is starting to catch up with the advent of IE 7. While there will probably always be discrepancies and differences of opinion about how this or that feature should work, they’re getting less obnoxious. However, when the spec itself is broken, what can browser makers do?

1. Inheritance

Listen, CSS. You have curly braces, why not use them? The cascade is a nice feature, as is the default inherit value for most properties, but they’re not enough. This would be so lovely:

.some-module {
      font-weight:bold;
      text-align:right;
      p {
            font-weight:normal;
            text-align:center;
      }
      h3 {
            font-size:200%;
      }
}

Instead, we have to type .some-module over and over for every bloody rule.

.some-module {
      font-weight:bold;
      text-align:right;
}
.some-module p {
      font-weight:normal;
      text-align:center;
}
.some-module h3 {
      font-size:200%;
}

If you use classes and descendant selectors properly on a sufficiently complicated page, you can get punished with very long selectors like this one from a site I’m working on now: .nav .search a, .nav .search input, .nav .search .button, .nav .search label. Every freaking rule in the file has .nav .search on the selector. This simple addition to the spec would have made it so much better:

.nav {
  .tabs {
    /* css for the "tabs" section of the .nav module */
  }
  .search {
    /* css for the "search" section of the nav module */
    a, input, .button, label {
      /* css rules */
    }
    a:hover, a:active {
      /* other CSS rules */
      /* note the huge reduction in code to be typed! */
    }
  }
}

Almost every language supports some version of this kind of semantic inclusion, but for some reason, CSS was created in such a way that every rule lives in its own universe. Lack of this feature makes CSS far less modular.

2. Vertical Alignment

Almost 2 million pages about centering vertically in CSS. This was so unbelievably easy back in the bad old days of layout tables. The browsers knew how to do it. But as of the second revision, the CSS spec still doesn’t even suggest a proper way to consistently vertically align one thing inside another.

Now, I know, there’s the vertical-align property. But, that only applies to inline, inline-block, and table-cell elements. If you want to align one box inside another, you’re stuck doing all sorts of hacking to make it work. If the heights are unknown ahead of time, it gets even more complicated.

This wouldn’t be such a huge problem, except that:

3. Table display values should have been done far sooner

OK, so step into the wayback machine, and travel to the 90s. You’re looking at an internet that is filled to the brim with poor abused tables. “Web developer” basically means “someone who knows how to use tables and font tags.”

So why did we have to wait until CSS2 before there was a way to emulate the behavior of the table tags? If display:table and display:table-cell had been in the first release of CSS, the standards revolution would have gone off without a shot. That’s all it would have taken for the hoards of table masters to be swept up in the semantic content/style division paradigm. Instead, CSS asked them to grasp “floating” and “position” and “overflow”, which are remarkably non-intuitive concepts when it comes to laying out content on a page.

By contrast, a grid that holds contents is extremely intuitive. That’s how typesetters had been laying out physical documents for millennia. That’s why <table cellspacing="0"> was such a godsend.

4. z-index is WAY too complicated

If you know CSS, there’s a good chance you know about z-index, or at least, think you do. Odds are about 4 to 1 that it doesn’t work how you think it does. If you’re like most web developers, rather than really understand it, you’ve probably just messed with the item in question until the right things showed up. (It doesn’t help that there are tutorials out there that are deeply wrong when it comes to z-index. If only it was as simple as w3schools makes it out to be!)

A decent summary can be found the Mozilla Developer Center:

In summary:

  • Positioning and assigning a z-index value to an HTML element creates a stacking context.
  • Stacking contexts can be contained in other stacking contexts, and together create a hierarchy of stacking contexts.
  • Each stacking context is completly [sic] independent from its siblings: only descendant elements are considered when stacking is processed.
  • Each stacking context is self-contained: after the element’s contents are stacked, the whole element is considered in the stacking order of the parent stacking context.

Notes:

  • The hierarchy of stacking contexts is a subset of the hierarchy of HTML elements, because only positioned and z-indexed elements create stacking contexts. We can say that elements that do not create their own stacking contexts are assimilated by the parent stacking context.

That’s right, this is just the summary. The full discussion is much longer and more complicated.

So, if element A contains X and Y, and element B contains P and Q, and A is above B, then X and Y are also above P and Q, even if P and Q have a much larger z-index than A, X, or Y. But, only if A is positioned. The idea is to keep any stacking “local” between siblings, so that the stacking of parent elements can be ignored. I wouldn’t be surprised if some browser maker lobbied for this rule because it made their Dom-walking algorithm simpler. And I can imagine some strange hypothetical super-modular layout situations where it might be handy. But it’s wrong, and exceedingly problematic for the vast majority of situations. In reality, it leads to newer web developers thinking, What the hell? I gave this thing z-index 99, and that other thing has z-index:1, and it’s above it?

Why? Why make it so complicated? Back when I was doing technical documentation at a software company, I had a saying: if it’s hard to explain, it’s probably badly designed. Partly out of laziness, and partly out of responsibility, I’d submit bugs to the developers when something was just too damn complicated. Here’s a better way:

  • Any element that is not display:none has a z-index.
  • Valid z-index values are either an integer, like 7, or the word inherit and optionally an integer modifier, such as inherit 2 (2 higher than whatever the parent is) or inherit -1 (1 lower than the parent). If inherit is used without a number, it defaults to inherit 0.
  • Elements that don’t have an explicit z-index are assumed to have the z-index of their parent. (I.e., z-index:inherit 0.)
  • Elements stack on the page according to their computed z-index.

How hard is that to understand? You can still use stacking contexts if you want, by using relative values, but without all the complications of having to trace through your page and figure out which parent needs to get position:relative in order to make your overlay lay over the rest of the page.

The day I grokked z-index is the day I realized that CSS is an insane and over-engineered language.

Laurie points out that If you have a drop-in html widget that’s all nicely z-ordered, you don’t want it to accidentally be interleaving with other elements on that page. I’m sure this was the thinking behind the stacking context in the first place. However, you would still be able to accomplish this with the inherit values. Just give each module a z-index in multiples of 100, and then use relative z-indexes within the modules. With all due respect, Laurie, as far as FooHack is concerned, the existing z-index paradigm is wrong, and CSS is being sent to the corner to think about what it’s done.

5. The Quirks Box Model is Right

Let’s say that you have a bookshelf that you’re going to put in your home. You measure the space on the wall to know how big of a bookshelf you can fit in the room. You find that there’s a space 34 inches across. When you go to the store, you get one that says its 33 inches wide, and bring it home, and assemble it, and it doesn’t fit! It’s an extra inch too wide, and now your closet door can’t open. You take it back to the store, and the salesperson calmly tells you, Well sir, the edge of the bookshelf is an inch wide. You see, you can only fit 33 inches of books on this shelf. You’d probably be quite perplexed, annoyed, and begin to wonder what kind of fairy land you’ve fallen into where they measure the insides of things first, and the outsides only as an afterthought.

In a rare fit of reasonableness, MSIE 5 did it right (and so does MSIE 6 when in Quirks Mode.) If you set the width to 100px, then it’s 100px. If you put 10px of padding on the box, then it leaves 80px inside for the text.

CSS disagrees with reason, however, and the spec says that a 100px wide box with 10px of padding will *actually* be 120px across, because the width of an element specifies the width of the inside, not the width of the element.

For images, this makes sense. If an image is 50px wide, and has 10px of padding, then you’d expect the width to specify the width of the asset itself (the image file) and the padding to get added onto that. But it would be a far far better decision to make it a little bit tricky for the rare case where you specify the width of images in the CSS rather than tricky for the much more common case where you specify the width of anything else.

The MSIE Quirks width was right. Too bad its dead and gone. Now, simply to get around what, in my opinion, is a horrible paradigm, I never set width or height on the same module where I set borders and padding. Width goes on the parent, and then borders and padding go on a width:auto child.

Other suggestions that didn’t make the list…

  • style block is invalid in the body, just annoying
  • min-height and min-width being unsupported
  • one background per element
  • no background clipping
  • no background-opacity distinct from element opacity
  • % font sizes being relative to parent, not base
  • border treatments

Special thanks to my esteemed colleagues who weighed in on this one.