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

Tweet

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.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.dan-schulz.com Dan Schulz

    That is pretty nifty.

  • http://www.kellishaver.com/ KelliShaver

    Neat trick, and something I didn’t know about, but most sites that are going to need such functionality will likely be doing other things that require the use of a javascript library of some sort anyway, and you can do the same thing in JS with much fewer lines of code, and it will be cross-browser compatible.

    Still, from a theoretical perspective, and a look at the sort of future potential this may hold, it’s quite interesting.

  • memco

    This seems kinda cool, but I can’t think of a time when this could be used in my code. I suppose if you can use sibling selectors then you could perhaps do table striping or something, but ultimately, it’s pretty limited.

  • zachleat

    Oh sweet, CSS expressions are back.

    Wait, what?

  • http://MeitarMoscovitz.com/ Meitar

    I can foresee some situations where something like this might be useful. If you combine this with something like

    ol:hover:after { /* display counters */ }

    then you get a rollover for the “Number of items in this list” example. And you can certainly use sibling selectors, as long as the browser supports them and counters, of course.

    You’re right, memco, that it’s kind of limited. I think this is partially by design and partially because CSS implementations just aren’t quite up to scratch yet. zachleat’s point about CSS expressions being less than perfect is also a good point, which is why I do think of this particular example as more of a theoretical showcase than anything for now. That said, I think the separation of content and presentation and, therefore, CSS expressions has more to do with design intent than it does technical ability, and from that perspective I don’t think CSS expressions should be universally considered a Bad Thing.

  • http://elysiansystems.com/ leadegroot

    If the argument promoting the neat technique is “Replacing executable scripts with style sheets often improves performance” then I’d like to see some performance benchmarks to show that it is so.

    And “results in a more accessible page”? I think experience has shown us that we need to be very careful in assuming a new technique makes a page more accessible rather than less. If the text may not be selectable and may not appear in the DOM… how likely is it that it improves accessibility?

    Sorry to seem catty. Its definitely cool :) but practical and useful? That remains to be seen.

  • http://www.sitepoint.com/articlelist/537/ Meitar

    @leadegroot: No worries, your comment didn’t strike me as catty, and I appreciate the feedback.

    Perhaps I wasn’t clear enough in my introduction that I intended to simply state how there are many examples of replacing JavaScript with CSS-based techniques, such as Eric Meyer’s early css/edge examples, and that the general trend of doing so can often be beneficial. I’ve no evidence that this particular technique is anything beyond kinda neat and definitely makes you think about CSS and its capabilities. :)

    That said, benchmarks for this technique in particular would be interesting, too.

  • Anonymous

    is there any way to stop images being taken from a web page?

  • http://www.sitepoint.com/articlelist/537/ Meitar

    @Anonymous: The answer to your question is: fundamentally, no. Once an image is viewed by a user it is already on the user’s own machine.

    I’m not sure why you’re asking that on this post, however, since CSS counters have nothing to do with preventing image extraction from pages.