Smiling open-mounted baby

Mastering simple gives you control over the presentation of tags – <body>, <h1>, <p>, etc – that are vital for the consistent presentation of web pages. But as your designs become more complex, there’s a level of detail that can only be gained through the use of selectors with an awareness of context.

For example, take a simple <li> tag:

li {
	font-weight: 900;
}

The declaration above sets all list items on a web page to the heaviest font weight, without defining which <li> elements will be affected. As written, the declaration will alter the appearance of all list items, within both ordered and unordered lists.

Parents & Children

Looking closer at the structure of HTML, we can see that list items always exist within the context of a parent element. In the case of an unordered list, that structure looks like this:

<ul>
	<li>Item One</li>
	<li>Item Two</li>
	<li>Item Three</li>
</ul>

List items can be considered children of the <ul> element, like triplets nestled inside a pregnant woman’s belly. In turn, these list items are siblings of each other, brother and sister in the same womb.

To address elements more precisely in CSS, we can specify their parent:

ul li {
	font-weight: 900;
}

Now only list items that are inside unordered lists will appear bolded. Note that it is the element at the end of the selector that is affected by the CSS rules that follow. For that reason, descendant selectors are easiest to read backwards: that is, the rule should be read as “list items that are inside unordered lists look this way”.

Working Backwards Through The Family Tree

Widening our scope to encompass an entire HTML document, we can see that unordered lists exist within the <body> tag, which in turn lies inside the <html> element. Theoretically, our descendant selector could be written as follows:

html body ul li {
	font-weight: 900;
}

Extending the generational metaphor, this selector defines an entire family tree, from a “great grandparent” <html> element, through to the <body> and <li> child. While this will work, defining a descendant selector from the root element is hardly ever used: including the <body> and <html> ancestors is pedantic, and adds unnecessary bytes to our stylesheet. More importantly, the longer a descendant selector becomes, the less efficient it is. While performance differences in the browser are usually tiny (just a few milliseconds in most cases), they quickly add up.

We can further refine our CSS rule by combining it with other selectors. For example, a class selector:

ul.vital li {
	font-weight: 900;
}

Now only list items that exist within an unordered list with a class of vital will be styled as extra-bold.

As another example, an attribute selector, to style site navigation links that appear inside <li> elements:

ul[role=navigation] li a {
	text-decoration: none;
	text-transform: uppercase;
}

Meaning: links inside list items that are in an unordered list with an ARIA role attribute set to navigation will appear with no underlining and in all caps.

Pruning

Note that it is possible to skip descendants in your declarations: the code above could also be phrased as:

ul[role=navigation] a {
	text-decoration: none;
	text-transform: uppercase;
}

While writing the selector this way is more efficient, it’s also a little dangerous, as it is possible to miss an element that is vital for the browser to understand the correct context of a style. For that reason, I recommend leaving optional descendants in a selector until the end of development, when they can be removed as a step in CSS optimization.

Photograph by Gonzalo Merat, licensed under Creative Commons.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.