css specificity pt1
Some issues appear too simple, too obvious and even too common to notice. These are the issues that are often essential to determine the strength of a language. These are also the issues that need investigating to improve a certain language from the base up. I've written about a similar css nesting issue before, a couple of weeks ago I ran into a new one.
This article will do its best to map out the problem and will hint at several fixes and workarounds. None of them fool-proof or particularly implementor-friendly. A follow-up article will then take a closer look at the core issue we're dealing with, diving into the depths of the human mind to uncover the true problem at hand. Sounds exciting, no?
Oh, and one more little thing before I start. I'm assuming that people reading this are familiar enough with css specificity rules. If not, pelase do some catching up first.
the issue
/* base component ...................... */ .class ul li {background:...} .class ul li.first {background:none;} /* variant */ .class.variant ul li {background-image:...}
The code above gives you a simplified version of the issue we're looking at. Just think of this as the code of a horizontal navigation. The background images on the list items are there to create a visual separation between the navigation items (often nothing more than a boring vertical stripe). We've created a base component but the design dictates to create a variant with a different separator, so we've added a rule to change the background-image for the specific variant. That's where the trouble starts.
The thing I keep missing is that the weight of the second and third selector are exactly the same. Because the third selector is lower in the css code the first li
element in the variant will again show the newly declared background-image. This is definitely not what I intended. All I wanted to do was keep the original component as is and change the background-image used to separate the navigation items.
not so good solutions
I've experimented a little and came up with four workarounds, one of which is commonly used to counter this problem. None of them is actually any good so I won't be spending much time running through them. I guess that most of these are pretty self-explanatory anyway. In short:
- 1. change css order: put the variant rule above the basic component. This way the weight is still the same, but the base rule will come last in the source, winning the specificity battle. Messing around with the order of your css like that is pretty bad though.
- 2. increase weight: add a random (but working) class befor the second rule. This will increase its weight, but apart from fixing this particular issue it makes no sense at all to do so.
- 3. !important: Add !important to the background declaration in the second rule. Another abuse for the !important rule, so nothing I will recommend.
- 4. duplicate styles: The most common solution, simply add a fourth rule that disables the background again for the variant. This means duplication of unnecessary css code, which I also dislike.
None of the solution above are considered extremely harmful (well ... maybe only the third solution), but seeing all the effort I put into maintaining a clean and readable css file, there isn't one solution I'd consider good practice. So let's go on to some more advanced solutions.
advanced solutions
You can read "advanced" as "not working (properly) in every browser". The key to fix this particular issue is to make sure that you only target the elements that need a background in the first place. Rather than overrule the background property of the first list element, let's make sure it never receives this background property at all. For that, we need some advanced css combinators.
1/ .class ul>li+li {background:...} 2/ .class ul>li~li {background:...}
Above are two variants on the same concept. The '>' combinator makes sure you're only targeting element on the same level, the '+' and '~' combinators both exclude the first (in this case) list element. Depending on the structure of your html you can chose what works best, though option 2 is definitely my favorite (as the ~combinator expresses exactly what we're aiming for). If you're working with different elements on the same level you can simply substitute "li" with "*".
This of course won't work in IE6, so if you still need to support that browser you can either leave this solution be or write some IE6 specific code in a separate css file. This means more work at first but a better css file when you can finally eliminate support for IE6 (and that time is definitely nearing). Your call.
conclusion
Even though this issue is not impossible to fix using some more advanced css selectors, there's still an underlying issue that remains. There is more happening here than simply bad browser support, but to really get to the bottom of this I'll post a follow-up article in the near future. So check back in a short while to read why an issue like this can keep creeping up on us.