Advanced Tree Counting: Mathematical Layouts With `sibling-index()` And `sibling-count()`<\/h1>\nDurgesh Pawar<\/address>\n 2026-05-21T08:00:00+00:00
\n 2026-05-21T20:44:30+00:00
\n <\/header>\n
You know that thing where you have a grid of cards, and you want them to fade in one after another? That staggered cascade effect. Looks great. Should be simple. And yet every time I\u2019ve built it, the implementation has made me feel like I\u2019m doing something fundamentally stupid.<\/p>\n\nSee the Pen [Dynamic Staggered Animations with CSS sibling-index() [forked]](https:\/\/codepen.io\/smashingmag\/pen\/zxowBog) by Durgesh<\/a>.<\/p>See the Pen Dynamic Staggered Animations with CSS sibling-index() [forked]<\/a> by Durgesh<\/a>.<\/figcaption><\/figure>\nBecause the options were always the same. Say you want staggered animation delays on a list of 10 items. You either wrote a Sass loop that spat out a dozen :nth-child()<\/code> rules, each one hardcoding a --index<\/code> variable for that specific position:<\/p>\n\/* One rule per item. Hope the list never grows. *\/\nli:nth-child(1) { --idx: 1; }\nli:nth-child(2) { --idx: 2; }\nli:nth-child(3) { --idx: 3; }\n\/* ... eight more of these ... *\/\nli:nth-child(10) { --idx: 10; }\n\nli {\n animation-delay: calc(var(--idx) * 100ms);\n}\n<\/code><\/pre>\nTen items. Ten rules. If the list grows to 50? You cap it and hope for the best, or set up a Sass loop that generates hundreds of selectors at build time. Engineers like Roman Komarov have come up with O(\u00e2\u02c6\u0161N) strategies<\/a> — legitimately clever stuff — but you still end up with 63 rules to cover 1,023 elements.<\/p>\nOr you looped through elements in JavaScript and set inline styles. style="--index: 3"<\/code>. Right there in the DOM. Works fine. Also spreads layout concerns across your scripts and quietly breaks six months later when someone refactors the component without realizing the CSS depends on a JavaScript-injected variable.<\/p>\nBoth approaches have always bugged me for the same reason: you\u2019re telling the browser something it already knows<\/strong>. The browser built<\/em> the DOM tree. It knows which element is the third child. It has the data. CSS just couldn\u2019t access it.<\/strong><\/p>\nWell, now it can:<\/p>\n
li {\n animation-delay: calc(sibling-index() * 100ms);\n}\n<\/code><\/pre>\nOne line. Works for 5 items or 5,000. No event listeners. No mutation observers. No re-renders.<\/p>\n
sibling-index()<\/code> and sibling-count()<\/code> are part of the CSS Values and Units Module Level 5<\/a> spec (Section 9, if you\u2019re the type who reads W3C drafts for fun). The proposal was approved via CSSWG issue #4559<\/a> after substantial discussion. The functions themselves take no arguments — you just use them.<\/p>\n\nsibling-index()<\/code><\/strong> gives you the 1-based position of an element among its parent\u2019s children. First child returns 1<\/code>. Fifth child returns 5<\/code>. It only counts element nodes — text nodes, comments, and whitespace are all invisible to it.<\/li>\nsibling-count()<\/code><\/strong> gives you the total number of element children the parent has. Basically, the CSS equivalent of element.parentElement.children.length<\/code> in JavaScript, but available in your stylesheet.<\/li>\n<\/ul>\nBoth functions resolve to <integer><\/code> — not <string><\/code>, an actual number. That means you can throw them into calc()<\/code>, min()<\/code>, max()<\/code>, round()<\/code>, mod()<\/code>, trigonometric stuff like sin()<\/code> and cos()<\/code><\/a>. When you write calc(sibling-index() * 100ms)<\/code>, CSS handles the type coercion and spits out a valid <time><\/code> value. No tricks needed. Compare that with counter()<\/code>, which returns a string and can only live inside content<\/code> on pseudo-elements — it\u2019s a different thing entirely.<\/p>\nOne clarification that trips people up: :nth-child()<\/code> is a selector<\/em>. It picks elements. It doesn\u2019t produce a value. You can\u2019t write calc(:nth-child() * 10px)<\/code> — that\u2019s not valid CSS. sibling-index()<\/code> does the opposite. It sits inside your declarations and gives you a number you can calculate with. They solve different problems, and until now we\u2019ve been duct-taping :nth-child()<\/code> into a role it was never designed for.<\/p>\nPatterns Worth Stealing<\/h2>\n
Once it clicks that these are just integers, ideas come fast.<\/p>\n
\n
\n 2026-05-21T20:44:30+00:00
\n <\/header>\n
See the Pen [Dynamic Staggered Animations with CSS sibling-index() [forked]](https:\/\/codepen.io\/smashingmag\/pen\/zxowBog) by Durgesh<\/a>.<\/p> Because the options were always the same. Say you want staggered animation delays on a list of 10 items. You either wrote a Sass loop that spat out a dozen Ten items. Ten rules. If the list grows to 50? You cap it and hope for the best, or set up a Sass loop that generates hundreds of selectors at build time. Engineers like Roman Komarov have come up with O(\u00e2\u02c6\u0161N) strategies<\/a> — legitimately clever stuff — but you still end up with 63 rules to cover 1,023 elements.<\/p>\n Or you looped through elements in JavaScript and set inline styles. Both approaches have always bugged me for the same reason: you\u2019re telling the browser something it already knows<\/strong>. The browser built<\/em> the DOM tree. It knows which element is the third child. It has the data. CSS just couldn\u2019t access it.<\/strong><\/p>\n Well, now it can:<\/p>\n One line. Works for 5 items or 5,000. No event listeners. No mutation observers. No re-renders.<\/p>\n Both functions resolve to One clarification that trips people up: Once it clicks that these are just integers, ideas come fast.<\/p>\n:nth-child()<\/code> rules, each one hardcoding a --index<\/code> variable for that specific position:<\/p>\n\/* One rule per item. Hope the list never grows. *\/\nli:nth-child(1) { --idx: 1; }\nli:nth-child(2) { --idx: 2; }\nli:nth-child(3) { --idx: 3; }\n\/* ... eight more of these ... *\/\nli:nth-child(10) { --idx: 10; }\n\nli {\n animation-delay: calc(var(--idx) * 100ms);\n}\n<\/code><\/pre>\nstyle="--index: 3"<\/code>. Right there in the DOM. Works fine. Also spreads layout concerns across your scripts and quietly breaks six months later when someone refactors the component without realizing the CSS depends on a JavaScript-injected variable.<\/p>\nli {\n animation-delay: calc(sibling-index() * 100ms);\n}\n<\/code><\/pre>\nsibling-index()<\/code> and sibling-count()<\/code> are part of the CSS Values and Units Module Level 5<\/a> spec (Section 9, if you\u2019re the type who reads W3C drafts for fun). The proposal was approved via CSSWG issue #4559<\/a> after substantial discussion. The functions themselves take no arguments — you just use them.<\/p>\n\n
sibling-index()<\/code><\/strong> gives you the 1-based position of an element among its parent\u2019s children. First child returns 1<\/code>. Fifth child returns 5<\/code>. It only counts element nodes — text nodes, comments, and whitespace are all invisible to it.<\/li>\nsibling-count()<\/code><\/strong> gives you the total number of element children the parent has. Basically, the CSS equivalent of element.parentElement.children.length<\/code> in JavaScript, but available in your stylesheet.<\/li>\n<\/ul>\n<integer><\/code> — not <string><\/code>, an actual number. That means you can throw them into calc()<\/code>, min()<\/code>, max()<\/code>, round()<\/code>, mod()<\/code>, trigonometric stuff like sin()<\/code> and cos()<\/code><\/a>. When you write calc(sibling-index() * 100ms)<\/code>, CSS handles the type coercion and spits out a valid <time><\/code> value. No tricks needed. Compare that with counter()<\/code>, which returns a string and can only live inside content<\/code> on pseudo-elements — it\u2019s a different thing entirely.<\/p>\n:nth-child()<\/code> is a selector<\/em>. It picks elements. It doesn\u2019t produce a value. You can\u2019t write calc(:nth-child() * 10px)<\/code> — that\u2019s not valid CSS. sibling-index()<\/code> does the opposite. It sits inside your declarations and gives you a number you can calculate with. They solve different problems, and until now we\u2019ve been duct-taping :nth-child()<\/code> into a role it was never designed for.<\/p>\nPatterns Worth Stealing<\/h2>\n