A Little-known Way to Replace Some Scripts with CSS Counters

Share this article

Ever since Eric Meyer highlighted how capable CSS is at his css/edge collection, we’ve been looking for ways to replace effects that were once only possible using JavaScript with CSS-based equivalents. Replacing executable scripts with style sheets often improves performance and results in a more accessible page. One of the earliest examples of this was Eric’s Pure CSS Popups, an effect he briefly summarized as text appears and disappears without JavaScript to drive it.

As CSS advances and implementations improve, more and more is possible purely using style sheets and without the need for additional scripting on either a site’s front- or back-end, which reduces the amount of executable code. One possibility that dawned on me recently was that, using only bits of CSS2.1’s generated content properties, we can aggregate and expose supplemental information about whole portions of pages.

Like many other blogs, SitePoint’s blog posts have a small paragraph of metadata at the end of each post. The paragraph gives readers supplemental information about the post, like its publish date, its tags, and any categories the post is filed under. Let’s see how easily we can add new information to this using only CSS.

Using (and abusing?) CSS counters

One feature of CSS2.1 that has remained underutilized for a long time is CSS counters. Counters are a subset of the CSS specification’s generated content sections. They are ostensibly a generic mechanism for numbering groups of elements that appear in a document tree. Sadly, support for this CSS feature has been lacking from certain browsers for a very long time.

What’s really interesting about CSS counters is that since the act of displaying a counter is decoupled from the counting mechanism itself, we can utilize them to display total counts of elements in addition to merely numbering elements in a series.

As a brief refresher, here’s how the CSS2.1 specification says we can replace the numeric markers of a typical ordered list with a CSS counter to achieve a more-or-less equivalent visual result:

ol { counter-reset: item; }
ol li { display: block; }
ol li:before {
    counter-increment: item;
    content: counter(item)". ";
}

This code initializes a counter called item at every ol element. Then it turns all the li elements into block-level CSS boxes instead of their default list-item boxes so they won’t show a marker (i.e., a number or bullet from the list-style-type property). Finally, for each list item, it increments the item counter, and then renders its current value.

Now, here’s how we can use the same mechanism to display the number of total list items at the end of the list with only minor and perfectly valid modifications:

ol { counter-reset: item; }
ol li {
    display: block;
    counter-increment: item;
}
ol:after {
    display: block;
    content: "Number of items in this list: " counter(item) ".";
}

The only “trick” is not to call the counter() function more than once. Specifically, you call it only :after you’ve finishing incrementing the counter at each element you’re counting. Used in this way, you can see that CSS counters are like a limited sort of integer variable.

Thanks to these two generalized capabilities—CSS counters and CSS generated content—we can start to get really creative.

Getting creative: Counting things in a blog post

One interesting use for this technique is to extend or, perhaps in some cases, completely replace programmatic code such as JavaScript or server-side scripts that count things. For example, here’s how I might use the technique above to count the number of distinct sections in a SitePoint blog post:


/* Initialize counter to 1 (not 0) since titles are outside .entrytext but may include intro paras. */
.post { counter-reset: num-post-sections 1; }
.post .entrytext h2 { counter-increment: num-post-sections; }
#thisentry:after {
    content: "This entry has " counter(num-post-sections) " sections.";
}

The code above simply increments a counter called num-post-sections each time a h2 element is encountered in the entrytext of a post, and then displays the result at the end just like the previous list item counting example.

Of course, you’re not limited to merely counting one thing. Here’s how I might count the number of sections and the number of code excerpts in a SitePoint blog post using the same pattern:


.post {
    counter-reset:
        num-post-sections 1 /* titles outside .entrytext but may include intro paras */
        num-code-listings
    ;
}
.post .entrytext h2 { counter-increment: num-post-sections; }
/* match both with (pre>code) and without (table.dp-c) JavaScript code highlighting */
.post .entrytext pre > code, table.dp-c { counter-increment: num-code-listings; }
#thisentry:after {
    content:
        "This entry has " counter(num-post-sections) " sections and "
        counter(num-code-listings) " code listings."
    ;
}

Screenshot showing this SitePoint blog post with the CSS excerpt applied.
Screenshot showing this SitePoint blog post with the CSS excerpt applied.

As a pattern, this becomes more useful if we generalize the above SitePoint-specific CSS rules so that they will work for any blog whose posts are structured using the hAtom microformat, so you can use it across many sites or insert it into your own browser as a user style sheet. The only necessary changes are the CSS selectors, but you can get very fancy. For the sake of example, I’ve thrown in a bunch of additional counters to illustrate more possibilities of what you can count.

/* Initialize counters. */
.hentry {
    counter-reset:
        num-post-sections
        num-code-listings
        num-code-listings-css /* code listings that are specifically CSS */
        num-links
        num-links-internal    /* links to other blog posts on this site */
        num-links-rel-tag     /* rel-tag microformat */
    ;
}
/* Increment counters. */
.hentry h2,
.hentry h3,
.hentry h4,
.hentry h5, /* consider any headline an additional "section" */
.hentry h6 { counter-increment: num-post-sections; }
.hentry pre > code { counter-increment: num-code-listings; }
.hentry pre > code.css {
    counter-increment:
        num-code-listings     /* increment count of total code listings */
        num-code-listings-css /* AND the subset that are just CSS samples */
    ;
}
.hentry :link { counter-increment: num-links; }
.hentry :link[href^="/blogs/"] {
    counter-increment:
        num-links
        num-links-internal
    ;
}
.hentry :link[rel="tag"] {
    counter-increment:
        num-links
        num-links-rel-tag;
}
/* Display results. */
.hentry:after {
    display: block;
    content:
        "This entry has a total of "
        counter(num-post-sections) " sections, "
        counter(num-code-listings) " code listings "
        "(" counter(num-code-listings-css) " are CSS) "
        " and " counter(num-links) " links, "
        "of which " counter(num-links-internal) 
        " point to other blog posts on this site "

        " and " counter(num-links-rel-tag) " are tags."
    ;
}

Naturally, you can only count what you can target with a CSS selector, so generating a word count isn’t possible. Also, you can only display totals in generated content that comes :after all of the markup you want to count, not :before it. Obviously, this implementation detail can limit the design flexibility you have.

Finally, it’s worth stressing again that CSS counters are not implemented in IE 6 or 7 (sigh…). Further, since we’re dealing entirely with CSS generated content, the displayed text may not be selectable by the user or accessible via the DOM for further manipulation. And of course, in many instances you may want to put such things directly into your markup as “real” content.

A point perhaps more important than these limitations, however, is that this is a powerful demonstration of how you can use your markup as an API to let the advancing capabilities of CSS do things you could once only do with client- or server-side scripting. Now that’s exciting.

Meitar MoscovitzMeitar Moscovitz
View Author
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form