CSS (Sass) for Content-Agnostic Numbered Headings

You’re writing a blog post or other HTML document and you want to number your h2 and h3 elements to give users a sense of where they are in the page’s logical flow. Or perhaps you’re maintaining legal content or technical documentation that needs to have numbered h2, h3, etc elements. I suppose you could nest the content of your entire document in a bunch of nested lists, or you could write the outline-style numbering by hand. Or … perhaps there’s a better way that only requires CSS.

We’ll start by checkout the code that makes this happen. What you see below is a Sass mixin, but you can do this with just the CSS output if you like.

@mixin generate-outline($reset-element: body, $list-style: decimal) {
  $counter: unique-id();

  counter-increment: $counter;

  &::before {
    content: counter($counter, $list-style)'. ';
  }

  @at-root {
    #{$reset-element} {
      counter-reset: $counter;
    }
  }
}

// How to use it
h2 {
  @include generate-outline(h1, upper-alpha);
}

h3 {
  @include generate-outline(h2);
}

Setting Up Defaults

You’ll use the generate-outline() mixin on each selector you want to number. The mixin takes two optional arguments: $reset-element and $list-style. $reset-element is the selector that you want to restart your numbering at. For example, if you want to number your h3 elements, but restart that count everytime your markup includes an h2, you would call @include generate-outline(h2); in your h3 declaration block. If you leave $reset-element empty, the mixin will default to body.

The second argument is $list-style and should be a valid value for the CSS list-style-type property. If you leave it blank, $list-style will default to decimal.

Using CSS Counters

The Sass mixin generate-outline() automates the use of CSS counters. There are 3 CSS properties we need to use to see counters in action.

Each level of counter needs a unique name. In the Sass above, we’re creating that name by using the Sass unique-id() function. If you’re just using CSS, you’ll need to choose a different name for each level of numbering.

We can increment that counter by using counter-increment: $name; on the element that’ll show the counter’s number.

We’ll use the ::before pseudoelement and the CSS content property to display that counter on each heading: content: counter($name)'. ';. Notice that we’re putting the string '. ' on the end of each number. This makes sure we get “A. Heading” instead of “AHeading.”

For the 2nd level of numbered headings, we’ll need to reset that counter every time an element in the 1st level of headings goes by. The Sass mixin gets the position of the current level in the $headings map - if it’s a 2nd (or deeper) level counter, the mixin will get the next element above it, and use that element to reset the counter. If the current element is the first element in the loop, the mixin will just attach counter-reset: $name; to the body element.

Output: Numbered Headings

With the CSS we’ve just generated, we’ve attached an A, B, C numbering system to all h2 elements in the page, and a 1, 2, 3 numbering system to all h3 elements in the page. This system doesn’t care what else you’ve got in your page: any paragraphs, blockquotes, other heading levels, images, etc – none of that affects this CSS’s ability to number h2 and h3 elements sequentially.

You can see this code in action on this CodePen.

See the Pen Sass / CSS for Numbered Document Headings by James Steinbach (@jdsteinbach) on CodePen.