islovely

Understanding CSS hierarchy-matching

by Dom Habersack (@soverydom), published Dec 27, 2012

To get a feel for how CSS-selectors should be written, you need to understand how browsers match them to the related markup. The following tries to give a top-level description that might not be entirely accurate but should still give you a good enough idea of how CSS and HTML get combined during rendering.

Note: Writing this, I realized that this is probably not how it actually works, as using regular expressions to match selectors and hierarchies would obviate the need for repeated identification-cycles. Nevertheless, thinking of the process this way helps writing simpler selectors, so the model remains valid.

Finding hierarchies

Consider a site with the following markup:

<html>
  <head>
    <!-- omitted -->
  </head>

  <body>
    <header>
      <h1>Site title</h1>
    </header>

    <nav>
      <ol>
        <li>
          <a href="#">Link</a>
        </li>
        <li>
          <a href="#">Link</a>
        </li>
      </ol>
    </nav>

    <section>
      <article>
        <header>
          <h2>Article title</h2>

          <p>Date</p>
        </header>

        <p>Content</p>

        <p>Content with <a href="#">link</a></p>

        <ul>
          <li>
            <a href="#">Link</a>
          </li>
          <li>
            <a href="#">Link</a>
          </li>
        </ul>
      </article>
    </section>
  </body>
</html>

When discarding the content and chaining all tags together according to their nesting, the path to each tag can be extracted:

Tag Hierarchy
html html
body html body
header html body header
h1 html body header h1
nav html body nav
ol html body nav ol
li html body nav ol li
a html body nav ol li a
li html body nav ol li
a html body nav ol li a
section html body section
article html body section article
header html body section article header
h2 html body section article header h2
p html body section article header p
p html body section article p
p html body section article p
a html body section article p a
ul html body section article ul
li html body section article ul li
a html body section article ul li a
li html body section article ul li
a html body section article ul li a

Note that some tags share the same hierarchy.

While each hierarchy could be used as a (bad) CSS-selector, think of them as an attribute we add to each tag that we now match against selectors.

Matching simple selectors

Selectors are matched to markup from right to left. Single-tag selectors are simplest and instruct the browser to ignore all elements whose hierarchy does not end in the exact tag given.

For example, matching the above table against the selector li would trim it down to just four elements:

Tag Hierarchy
li html body nav ol li
li html body nav ol li
li html body section article ul li
li html body section article ul li

The browser only had to filter the initial table a single time to find these elements and can now apply the styles set with the selector.

Matching multi-level selectors

As soon as we chain selectors the browser has to iterate over intermediate results multiple times, potentially filtering them with every step.

When applying a rule such as ul > li, the browser again starts at the rightmost tag and filters the hierarchy-table to end up with the same result as before:

Tag Hierarchy
li html body nav ol li
li html body nav ol li
li html body section article ul li
li html body section article ul li

This list now gets filtered again to identify all li-tags nested directly below a ul-tag:

Tag Hierarchy
li html body section article ul > li
li html body section article ul > li

The more levels there are in a selector, the more times each resulting list needs to be iterated over again.

While extending a selector will often result in a condensed list, unnecessary extensions generate wasted filtering-cycles. In the above example, adding html > body > section > article > in front of the selector would force the browser to filter the result four more times without altering the list before it could assert a match.

Identification loops

While every level adds a new iteration over a filtered list of potentially-matching tags, some selectors require a more complex analysis of a single element’s hierarchy. While the child-selector (>) only requires a check of the direct parent tag, a descendant-selector (space) might force the browser to walk up the entire hierarchy trying to find a match.

For example, nav a first filters out all elements whose hierarchy does not end in an a-tag, resulting in the following list:

Tag Hierarchy
a html body nav ol li a
a html body nav ol li a
a html body section article p a
a html body section article ul li a
a html body section article ul li a

When adding nav to the selector, the browser has to execute a more detailed analysis of each element’s hierarchy. For the first element, it has to go through the following steps:

Iteration Hierarchy Result
1 html body nav ol li a nav not found, continue…
2 html body nav ol li a nav not found, continue…
3 html body nav ol li a nav found, match

The browser can stop walking up this element’s hierarchy as soon as a match is found and continue with the next element. In contrast to that, the last element from the initial list gets matched against nav a as follows:

Iteration Hierarchy Result
1 html body section article ul li a nav not found, continue…
2 html body section article ul li a nav not found, continue…
3 html body section article ul li a nav not found, continue…
4 html body section article ul li a nav not found, continue…
5 html body section article ul li a nav not found, continue…
6 html body section article ul li a nav not found, no match

For elements that only match the end of a selector but not its beginning, the browser has to walk up the entire hierarchy before it can assess that the element definitely does not match.

In real-world examples, pages usually contain a much higher number of elements with deeper and more complex hierarchies. Since the matching-process has to be repeated for every element remaining after each filtering-step, it is best to keep nesting of markup to a minimum and use selectors that match or fail as quickly as possible.

You can read more recommendations on doing the latter in my post on writing high-performance CSS.