Skip to content

Taming nested OLs with 1 nested CSS rule

  • Published: 2024-02-04 15:54
  • Updated: 2024-02-07 15:50

I recently wondered—With how few CSS rules can I tame nested, ordered lists of any depth:

  • In a fluid and responsive way
  • That adapts to @media declarations
    • Without further modifications
  • Whilst sticking to a baseline grid (as close as possible)
  • And subtle tweaks to optimize legibility?

CSS nesting seemed like a perfect tool for the job. So, I had a go at codepen.io.

Drumroll—The CSS

ol {
  padding-left: 0;
  line-height: .975rlh;

    li {
        margin-left: 2ch;
        margin-bottom: .125rlh;  
    }

    ol li:first-child {
        margin-top: .125rlh;
    }
}

Kind of underwhelming, isn’t it? And yet, if I’m not mistaken that’s already it. Guess it makes sense to see it in action, before dissecting it:

In action

  1. We’re curious about this ‘thing'
    1. This aspect isn’t too fancy
    2. So, let’s explore this aspect in detail
      1. Delving deeper, we uncover more nuances
        1. Continuing the exploration, we focus on a specific aspect
          1. Extending the discussion, we provide real-world examples
          2. Finally, we conclude this nested exploration with an extended summary, that must include a line-break
      2. Returning to the broader topic, we address related concerns
    3. Moving on, we examine another sub-topic within this section
      1. Sub-sub-section 1.1.2
      2. Sub-section 1.2
        1. Exploring a different perspective, we introduce a new concept
          1. Considering alternative viewpoints, we discuss varying opinions
          2. Wondering how deep the rabbit hole goes
            1. And how much further we can nest this list?
              1. What if we discovered something very important here that needed a lengthy description?
                1. And oh: yet another discovery awaits!
                  1. With that yet another level, so we’ve have to keep nesting

The nested CSS in detail

ol {
  padding-left: 0;
  line-height: .975rlh;
  • padding-left: 0; removes the standard padding
  • 1rlh equals the root elements’/global line-height
  • Removing .25rlh > .975lh subtly:
    • Helps separating li items including line-breaks
    • Creates opportunity to use the ‘stolen’ .25rlh for margins
    li {
        margin-left: 2ch;
        margin-bottom: .125rlh;  
    }
  • li selects every list-item that’s a child of ol
  • margin-left: 2ch; indents the items by two-letters
    • Centering the counters of nested levels below the first letter of adjacent list-content, creating a trackable horizontal flow
    • 1ch equals the computed width of the number 0
      • According to the context, thus font-size in use
  • margin-bottom: .125lh; returns the vertical separation, that we nicked from the overall line-height of the ol
    ol li {
        margin-top: .125rlh;
    }
  • ol li selects every first item of any nested level depth
  • margin-top: .125rlh; adds a subtle bit of further vertical separation between the items
    • Yes, the anally retentive baseline grid approach would be using .1lh for both margins, because we nicked .25rlh from the line-height
    • Having fiddled around with many different values, I think this is a good compromise between math and accessibility

The final margin bottom, depending on context

Depending on the overall flow of top-, and bottom-margins across the elements of your page, the final list-items margin won’t be set. In case the following paragraphs need vertical spacing:

    ol + p {
        margin-top: 1rlh;
    }
  • Sets a margin-top on the element following the OL
  • I didn’t find a non-hacky way of doing it otherwise, yet

Resources for the curious