CSS Smart Selectors, Part 1 of 2

Dave Gash
12 min readMar 13, 2018

--

It’s all about pattern matching

Introduction

I’ve been a programmer for a long time. I took “Jacquard Loom 101” with Ada Lovelace, and her brocades were always fancier than mine, even though she spent half her time writing notes to that nerd Babbage.

Back then — and for a century after — all computer instruction was done by procedural programming. In fact, most programming languages today are procedural, and that’s how we think of software development. Eventually, though, a new instruction model called declarative programming came along and changed the complexion of instructional code.

Whereas procedural programming is very linear and strictly step-oriented — “do this first, then this, then this…” — declarative programming is explicitly non-linear and very goal-oriented. Consider how constructing a model airplane differs from assembling a jigsaw puzzle. The model airplane’s instructions are absolutely procedural; they’re sequentially numbered for a good reason, and if you perform them out of order, you’ll end up with a wonky, error-laden model. But when you look for a jigsaw puzzle piece’s spot, where it fits into the picture’s pattern, you may or may not see it. If so, of course, you fit it into place — but if not, there are no errors, no undesirable consequences; you shrug, toss it back into the box, and try another piece. That’s declarative programming!

In this two-part article, we’ll look at CSS as a declarative language and explore how we can use its pattern-matching features to write smarter selectors and thus reduce our page maintenance time.

What’s the Problem?

Just so you don’t think this a solution looking for a problem, let’s take a moment to understand why smarter selectors are valuable. The basic problem is that, when using common CSS selectors, we incur significant HTML overhead and maintenance to support our CSS rules — specifically the addition and updating of multiple HTML class attributes to tell the rules which elements to modify. Take, for example, this ridiculously simple page (I’ve marked the HTML headings and paragraphs for clarity).

Let’s say, given the nature of the content, that it might be prudent to mark the “Cautions and Warnings” h3s for each subject section in red text, to make them stand out and cover our, um, assets from a legal standpoint. That’s simple enough; we just write a CSS rule to make them red.

h3 { color: red; }

Et voilà…

But wait a sec, now all the h3s are red — that’s not what we wanted!

Fine; it’s easy enough to fix the CSS. We just turn the generic h3 selector into a dependent class definition by adding a dot and a class name.

h3.caveats { color: red; }

Now, only h3s of class “caveats” will be red. But before we can use the new rule we have to go into our HTML and add the class attribute to each of the “Cautions and Warnings” h3s. Whether there are two or twenty of them, they all need the class in order for the rule to affect them.

<h3 class=”caveats”>(h3) Cautions and Warnings</h3>. . .<h3 class=”caveats”>(h3) Cautions and Warnings</h3>. . .etc.

And, sure enough…

Okay, that’s what we wanted. But, looking at the page again, we realize it might be even wiser to make not only the “Cautions and Warnings” h3s red, but their paragraph text as well. That way, no one can say we didn’t warn the reader, boldly and explicitly, about these (admittedly) stupid ideas. In our CSS, we just turn the dependent class into an independent class by removing the h3 part of the selector.

.caveats { color: red; }

But again, we know that before we can use the new rule, we now have to go add the class attribute to all of the paragraphs in all of the “Cautions and Warnings” sections just as we did for the h3s. Again, no matter how many paragraphs there are under each “Cautions and Warnings” h3, they all need the class.

<p class=”caveats”>(p) Make all down-line connections…</p>. . .<p class=”caveats”>(p) Acquire a new, in-box trampoline…</p>. . .etc.

And, finally…

Ah, at last: plenty of red warning headings and paragraphs, just like we wanted. But consider that, as easy as the CSS was to write, how much HTML maintenance we had to do to make it work. It was kind of a pain in the asset, yes? And that was just for one simple little rule!

That’s the problem.

And you think that was messy? Have a look at any non-trivial HTML page and you’ll see dozens, perhaps hundreds, of classes scattered all over the code.

Every one of those classes is there specifically to identify the elements that certain CSS rules should modify. And of course, as your CSS changes, so will your classes, and thus your HTML. More maintenance, more errors, more headaches. Eek!

Instead of having to explicitly and manually add classes to our HTML to identify the target elements for given CSS rules, it would be oh so convenient if CSS could work out for itself which elements to modify. But can CSS do that?

Well, duh; of course it can, or this article would end here, amirite?!?

CSS is All About Patterns

Selectors are just patterns that CSS tries to match in the HTML in order to select elements for modification by the rule. For example:

  • h3 { . . . } means select all h3s
  • p.info { . . . } means select only paragraphs with class=“info” (dependent class)
  • .tip { . . . } means select any elements with class=“tip” (independent class)

The big takeaway here is that smarter selectors mean better pattern matching, which means fewer classes are needed, which means less HTML maintenance.

So how do we define patterns without relying on classes? Mostly by referring to the physical structure of the elements in the HTML page. The structure is typically described in terms of the elements’ “familial” relationships. There are four simple terms to know here: parent, child, sibling, and descendant. Don’t let these confuse you; they mean exactly what you think they mean.

<h2>Check this out!</h2><div>  <p>This is a <i>bad</i> idea.</p>  <p>But that’s never stopped me before.</p></div><p>No, I mean a <b>really</b> bad idea.</p>

For example, in this chunk of HTML (deep breath!)… the div is the parent of the first two paragraphs; those two paragraphs are children of the div. The first paragraph is the parent of the italic tag; the italic tag is the child of that paragraph, and a descendant of the div. The last paragraph is the parent of the bold tag; the bold tag is the child of that paragraph. The h2, the div, and the last paragraph are siblings.

Note that the div is not a child of the h2; it merely follows the h2, and is not “inside” it. (In fact, the h2 has no children or descendants at all.) Nor is the last paragraph a child of the div or of the h2; it is actually at the same level as both and is therefore their sibling.

Again, don’t make this more complicated than it is. The terms are intuitive and semantically correct; we just need to use them properly when we talk about constructing CSS selector patterns to match HTML structure. To do this, we work out how to unambiguously identify certain HTML elements based on their relationships, and then formulate a CSS selector that describes that pattern. Easy-peasy! (No, really, it is.) And CSS offers us a bunch of clever ways to define the patterns.

Contextual Selectors

Contextual selectors, or “smart selectors” as I call them in this article, select HTML elements based on their context — their position in the page and their relationships to other elements — rather than on CSS classes. This lets us avoid both the definition of multiple classes in our CSS and their application via multiple class attributes in our HTML. The complementary benefits of this technique are localization of all style directives in the CSS, and reduced maintenance and attendant complexity in the HTML.

Contextual selector categories include combinators, pseudo-classes, pseudo-elements, and attribute selectors. In this Part 1 of a two-part article, let’s dive in to combinators and pseudo-classes.

Combinators

Combinators combine selectors, based on their context, to form a unique pattern by which HTML elements can be identified for modification by a CSS rule. The combinators (think of them in much the same way as operators in a math formula or programming statement) are four single characters, each of which combines two selectors (called “A” and “B” in the following examples) preceding a rule. That is, a combinator character defines a context for element “B” relative to element “A” that must match for the rule to be applied.

They are actually quite simple to use, although they can be a bit tricky to remember. I’ve used them for years, and still look them up most of the time just to be sure (seriously). There are two combinator characters for parent/child relationships and two for sibling relationships.

BTW, all of these examples use W3Schools.com’s excellent TryIt editor. Try it! :-)

Descendant selector: A B

This combinator character is — and I swear I’m not making this up — “ ” . That’s right, a space character. It’s in the line above, right there between the A and the B. See it? What, you can’t see it? Oh, that’s right, you can’t see it, because it’s a freaking space! Although that seems odd, this is probably the most frequently used combinator, so maybe it kind of makes sense that it’s the quickest and easiest to type. (That might not be the real reason, but it’s all I got.)

The space combinator selects all B elements that are descendants (either direct children or children of children) of A elements. Thus, div p { ... } selects all paragraphs that are descendants of divs in the page. Example:

← CSS/HTML | Rendered result →

Paragraph 3 is selected because it is a child of the blockquote, which is a child of the div; Paragraph 3 is thus a descendant of the div and matches the pattern.

Child selector: A > B

This combinator character is “>”, the greater-than sign, sometimes called a right angle bracket. Hey, at least you can see this one!

The > combinator selects all B elements that are immediate children (not descendants) of A elements. Thus, div > p { ... } selects all paragraphs that are direct children of divs in the page. Example:

Paragraph 3 is not selected here because it is a child of the blockquote, which is a child of the div; thus Paragraph 3 is a descendant of the div and does not match the pattern.

Adjacent sibling selector: A + B

This combinator character is “+”, the plus sign.

The + combinator selects all B elements that are direct (immediate) siblings of A elements. Thus, div + p { ... } selects all paragraphs that immediately follow divs as siblings. Example:

Only Paragraph 3 is selected here because it is the next adjacent sibling (not a child, a descendant, or a distant sibling) of the div. This combinator can be a bit confusing; I find it easier to grasp if I call it “first sibling”, because that’s what it selects — the first B sibling of the A element.

Adjacent sibling selector: A ~ B

This combinator character is “~”, the tilde.

The ~ combinator selects all B elements that are (subsequent) siblings of A elements. Thus, div ~ p { ... } selects all paragraphs that follow divs (immediately or later) as siblings. Example:

While Paragraph 3 and Paragraph 5 are selected as siblings of the div, Paragraph 4 is not selected; being a child of the blockquote, it is a descendant of the div, not a sibling.

Question

In the above four examples, how many HTML class attributes did we have to add to make all of that CSS work?

That’s right, baby!

Pseudo-classes

CSS pseudo-classes identify elements for selection by their state, or current condition, which is another way to determine their context. In a selector, pseudo-classes are appended to elements with a colon (:). That is, element:pseudo-class { . . . }.

There are many pseudo-classes, and if you work with CSS at all, you’re probably familiar with a few, such as :visited, :hover, or :focus.

Some are quite obvious, others less so. Here are a few examples of :hover as it changes some text attributes when the pointer (not shown) is hovering over a link:

Yes, I realize that we used CSS dependent classes (a.one, a.two, etc.) in the selectors just to differentiate the links so we could put them all on one page. Those classes don’t have anything to do with the pseudo-classes, so please don’t flame me for it, kthx. :-)

And here is an example of :focus, when a form input field is clicked in or tabbed to.

In all of these cases, the pseudo-class defines an element’s state, a condition that can change over the life of the page. CSS determines the context of the element and decides whether to apply the rule by constantly watching its state.

Finally, have a look at the :nth-child() pseudo-element. It’s a bit less intuitive than the others, but still makes sense; it defines an ordinal position (or a multiplier) to identify specific children of a parent. In this example, it’s used to select paragraphs that are the second children of their respective containers.

Here, “The second paragraph” really is the second child of its containing div, and “The fifth paragraph” is the second child of its containing blockquote, so they are both selected.

A very common use of :nth-child() is to make child’s play (YSWIDT?) of coding zebra tables, like this.

table tr:nth-child(odd) {  color: red;  background-color: yellow;}table tr:nth-child(even) {  color: yellow;  background-color: red;}

In these two rules, note the descendant selector combinator (that pesky space) between table and tr that tells the rule to select every table row in a table, the :nth-child() pseudo-class modifiers on the table rows, and the simple odd and even multiplier keywords instead of specific ordinal positions. Pretty spiffy; two simple rules, and hello, all your tables are zebra-fied.

I never said they’d be pretty

Question

Exactly how much HTML editing did we have to do in order to make all of that CSS work? (Don’t get this wrong.)

Right again!

Summary

In Part 1 of this article, we explained the HTML maintenance problem caused by over-reliance on CSS classes. We proposed a solution in coding smarter CSS rule selectors, and explored the first two kinds of contextual selectors, combinators and pseudo-classes.

In Part 2, we’ll cover the other two kinds of smart selectors, pseudo-elements and attribute selectors. Check it out!

--

--

Dave Gash

Dave is a retired technical publications specialist and has been a frequent speaker at User Assistance conferences around the world since 1998.