{"id":7347,"date":"2025-10-09T19:36:27","date_gmt":"2025-10-10T00:36:27","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7347"},"modified":"2025-10-09T19:36:28","modified_gmt":"2025-10-10T00:36:28","slug":"css-counters-in-action","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/css-counters-in-action\/","title":{"rendered":"CSS Counters in Action"},"content":{"rendered":"\n<p>A classic <code>for<\/code> loop:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">for<\/span> (int i = <span class=\"hljs-number\">0<\/span>; i &lt; <span class=\"hljs-number\">10<\/span>; i++) {\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For most of us, some variation of this code is one of the first things we learned when we were first starting out. For me it was C++, but just about any language has some version of it\u2014even CSS. Yes, CSS has counter variables!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"css-counters-in-action\">The Basics<\/h2>\n\n\n\n<p>CSS Counters are driven by four properties:&nbsp;<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><code>counter-reset<\/code><\/li>\n\n\n\n<li><code>counter-set<\/code><\/li>\n\n\n\n<li><code>counter-increment<\/code><\/li>\n\n\n\n<li><code>counter()<\/code><\/li>\n<\/ol>\n\n\n\n<p>Let&#8217;s say we wanted a React component that renders a few lines of text, where the number of lines is received as a prop. But we also want to display line numbers next to each line,&nbsp;<em>and<\/em>&nbsp;we want to use CSS to do so. That last assumption might seem silly, but bear with me; we&#8217;ll look at a real-world use case at the end.<\/p>\n\n\n\n<p>Here&#8217;s the component<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> NumberedSection: FC&lt;{ count: <span class=\"hljs-built_in\">number<\/span> }&gt; = <span class=\"hljs-function\">(<span class=\"hljs-params\">{ <span class=\"hljs-params\">count<\/span> }<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div&gt;\n      {<span class=\"hljs-built_in\">Array<\/span>.from({ length: count }).map(<span class=\"hljs-function\">(<span class=\"hljs-params\">_, <span class=\"hljs-params\">idx<\/span><\/span>) =&gt;<\/span> (\n        &lt;span key={idx}&gt;This is line&lt;<span class=\"hljs-regexp\">\/span&gt;\n      ))}\n    &lt;\/<\/span>div&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;ll use a CSS counter called&nbsp;<code>count-val<\/code>&nbsp;to manage our line numbers. In CSS, we can reset our counter for each and every&nbsp;<code>counter-container<\/code> <code>&lt;div&gt;<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.counter-container<\/span> {\n  <span class=\"hljs-attribute\">counter-reset<\/span>: count-val;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And then for each line inside that container, we can increment our counter, and render the current number in a pseudo-element.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.counter-container<\/span> <span class=\"hljs-selector-tag\">span<\/span><span class=\"hljs-selector-pseudo\">::before<\/span> {\n  <span class=\"hljs-attribute\">counter-increment<\/span>: count-val;\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counter<\/span>(count-val);\n  <span class=\"hljs-attribute\">margin-right<\/span>: <span class=\"hljs-number\">5px<\/span>;\n  <span class=\"hljs-attribute\">font-family<\/span>: monospace;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If we render two of these components like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;NumberedSection count={<span class=\"hljs-number\">3<\/span>} \/&gt;\n&lt;hr \/&gt;\n&lt;NumberedSection count={<span class=\"hljs-number\">4<\/span>} \/&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It will display numbered lines just like we want:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"284\" height=\"478\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1.png?resize=284%2C478&#038;ssl=1\" alt=\"CSS Counters working\" class=\"wp-image-7361\" style=\"width:197px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1.png?w=284&amp;ssl=1 284w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1.png?resize=178%2C300&amp;ssl=1 178w\" sizes=\"auto, (max-width: 284px) 100vw, 284px\" \/><\/figure>\n<\/div>\n\n\n<p>If you wanted to increment by some other value than 1, you can specify whatever <code>counter-increment<\/code> you want:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">counter-increment<\/span>: <span class=\"hljs-selector-tag\">count-val<\/span> 2;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And if you wanted to just&nbsp;<em>set<\/em>&nbsp;a counter to a specific value, the <code>counter-set<\/code> property is for you. There&#8217;s a few other options that are of course discussed on&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_counter_styles\/Using_CSS_counters\">MDN<\/a>.<\/p>\n\n\n\n<p>I know this seems silly, and I know this would have been&nbsp;<em>simpler<\/em>&nbsp;to do in JavaScript. The counter variable is already&nbsp;<em>right there<\/em>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"a-better-use-case\">A Better Use Case<\/h2>\n\n\n\n<p>Let&#8217;s get&nbsp;<em>slightly<\/em>&nbsp;more realistic. What if you have various headings on your page, representing section titles. And, as you might have guessed, you want them numbered.<\/p>\n\n\n\n<p>Let&#8217;s start by reseting a CSS counter right at the root of our page<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">body<\/span> {\n  <span class=\"hljs-attribute\">counter-reset<\/span>: tile-num;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we&#8217;ll increment and display that counter for each heading that happens to be on our page. And if we want custom formatting on the line numbers, we can list out strings, and CSS will concatenate them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">h2<\/span><span class=\"hljs-selector-class\">.title<\/span><span class=\"hljs-selector-pseudo\">::before<\/span> {\n  <span class=\"hljs-attribute\">counter-increment<\/span>: tile-num;\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counter<\/span>(tile-num) <span class=\"hljs-string\">\": \"<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now when we have some content:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>This is a title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>This is the next title on the page<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>This is a title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We&#8217;ll get line numbers next to each heading.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"554\" height=\"400\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2.png?resize=554%2C400&#038;ssl=1\" alt=\"\" class=\"wp-image-7364\" style=\"width:330px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2.png?w=554&amp;ssl=1 554w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2.png?resize=300%2C217&amp;ssl=1 300w\" sizes=\"auto, (max-width: 554px) 100vw, 554px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"one-last-example\">One Last Example<\/h2>\n\n\n\n<p>Before going, I&#8217;d like to share the use case that led me to discover this feature. So far the examples we&#8217;ve seen are either contrived, or better served by just using JavaScript. But what if you don&#8217;t have control over the markup that&#8217;s generated on your page?<\/p>\n\n\n\n<p>I recently moved my blog&#8217;s code syntax highlighting from <a href=\"https:\/\/prismjs.com\/\">Prism<\/a> to <a href=\"https:\/\/shiki.matsu.io\/\">Shiki<\/a>. Everything went well except for one thing: Shiki does not support line numbers. This created a perfect use case for CSS counters.<\/p>\n\n\n\n<p>I used my Shiki configuration to inject a&nbsp;<code>data-linenumbers<\/code>&nbsp;attribute onto any&nbsp;<code>pre<\/code>&nbsp;tag containing code I wanted numbered, and then I solved this with a little bit of CSS.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">pre<\/span><span class=\"hljs-selector-attr\">&#91;data-linenumbers]<\/span> <span class=\"hljs-selector-tag\">code<\/span> {\n  <span class=\"hljs-attribute\">counter-reset<\/span>: step;\n}\n\n<span class=\"hljs-selector-tag\">pre<\/span><span class=\"hljs-selector-attr\">&#91;data-linenumbers]<\/span> <span class=\"hljs-selector-tag\">code<\/span> <span class=\"hljs-selector-class\">.line<\/span><span class=\"hljs-selector-pseudo\">::before<\/span> {\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counter<\/span>(step);\n  <span class=\"hljs-attribute\">counter-increment<\/span>: step;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">1rem<\/span>;\n  <span class=\"hljs-attribute\">margin-right<\/span>: <span class=\"hljs-number\">1rem<\/span>;\n  <span class=\"hljs-attribute\">display<\/span>: inline-block;\n  <span class=\"hljs-attribute\">text-align<\/span>: right;\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">rgba<\/span>(<span class=\"hljs-number\">115<\/span>, <span class=\"hljs-number\">138<\/span>, <span class=\"hljs-number\">148<\/span>, <span class=\"hljs-number\">0.4<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Just like that, I had numbered lines<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"346\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3.png?resize=1024%2C346&#038;ssl=1\" alt=\"Code snippet of a JavaScript function named readBooks that processes a variable string to manage pagination and filtering for a list of books.\" class=\"wp-image-7365\" style=\"width:657px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3.png?resize=1024%2C346&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3.png?resize=300%2C101&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3.png?resize=768%2C260&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3.png?w=1408&amp;ssl=1 1408w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"odds-and-ends\">Odds &amp; Ends<\/h2>\n\n\n\n<p>We&#8217;ve covered all you&#8217;ll probably ever use of CSS counters, but for completeness let&#8217;s look at some tricks it supports.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"formatting-the-numbers\">Formatting the numbers<\/h3>\n\n\n\n<p>It turns out you can customize the display of the number from the CSS counter. The&nbsp;<code>counter()<\/code>&nbsp;function takes an optional second argument, detailed&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/counter#counter-style\">here<\/a>.<\/p>\n\n\n\n<p>For example, you can display these counter values as uppercase Roman numerals.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">counter<\/span>(<span class=\"hljs-selector-tag\">tile-num<\/span>, <span class=\"hljs-selector-tag\">upper-roman<\/span>)<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"294\" height=\"484\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img4.png?resize=294%2C484&#038;ssl=1\" alt=\"A visual representation of numbered lines in a code-like format, displaying CSS counters with line numbers I, II, III, and IV next to text lines.\" class=\"wp-image-7366\" style=\"width:206px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img4.png?w=294&amp;ssl=1 294w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img4.png?resize=182%2C300&amp;ssl=1 182w\" sizes=\"auto, (max-width: 294px) 100vw, 294px\" \/><\/figure>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"nested-counters\">Nested Counters<\/h3>\n\n\n\n<p>Remember the titles we saw before? What if those containers with the numbered titles could nest within each other.<\/p>\n\n\n\n<p>Take a look at this markup.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"nested\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>This is a title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>This is the next title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n    Content content content\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"nested\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>Nested title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>Nested title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n        Content content content\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"nested\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h4<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>Nested 2nd title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h4<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"title\"<\/span>&gt;<\/span>Last title<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>Content content content<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Do you see how those&nbsp;<code>nested<\/code>&nbsp;containers can &#8230;&nbsp;<em>nest<\/em>&nbsp;within each other? Each new nested container resets its counter. But wouldn&#8217;t it be neat if css could take all values from the current elements&#8217; ancestors, and connect them? Like a nested table of contents?<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"444\" height=\"566\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img5.png?resize=444%2C566&#038;ssl=1\" alt=\"\" class=\"wp-image-7368\" style=\"width:280px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img5.png?w=444&amp;ssl=1 444w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img5.png?resize=235%2C300&amp;ssl=1 235w\" sizes=\"auto, (max-width: 444px) 100vw, 444px\" \/><\/figure>\n<\/div>\n\n\n<p>Well it can! Let&#8217;s take a look at the css that produced the above.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.nested<\/span> {\n  <span class=\"hljs-attribute\">counter-reset<\/span>: nested-num;\n}\n<span class=\"hljs-selector-class\">.nested<\/span> <span class=\"hljs-selector-tag\">p<\/span> {\n  <span class=\"hljs-attribute\">margin-left<\/span>: <span class=\"hljs-number\">10px<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.nested<\/span> <span class=\"hljs-selector-class\">.title<\/span><span class=\"hljs-selector-pseudo\">::before<\/span> {\n  <span class=\"hljs-attribute\">counter-increment<\/span>: nested-num;\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counters<\/span>(nested-num, <span class=\"hljs-string\">\".\"<\/span>);\n  <span class=\"hljs-attribute\">margin-right<\/span>: <span class=\"hljs-number\">5px<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To achieve this we just use the&nbsp;<code>counters<\/code>&nbsp;function, rather than&nbsp;<code>counter<\/code>. It takes a second argument that tells CSS how to join the numeric values for all counter instances on the current element. It also supports a&nbsp;<em>third<\/em>&nbsp;argument (not shown) to allow you to alter the display of these numbers, like we did before with roman numerals.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"concluding-thoughts\">Concluding Thoughts<\/h2>\n\n\n\n<p>CSS counters are a fun feature that can occasionally come in handy. They\u2019re a useful feature to keep in the back of your mind: they might help you out one day.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>CSS has counter variables (based on matching selectors) that you can output as formatted content or use in calculations. <\/p>\n","protected":false},"author":21,"featured_media":7377,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[106,7],"class_list":["post-7347","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-counters","tag-css"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/CSS-Counters-in-Action.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7347","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7347"}],"version-history":[{"count":12,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7347\/revisions"}],"predecessor-version":[{"id":7380,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7347\/revisions\/7380"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7377"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7347"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7347"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7347"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}