- Published on
Experimenting with new widely available css functions sibling-index() and sibling-count()
sibling-index() and sibling-count() became widely available in August 2026, when Firefox added support. As the names suggest, sibling-index() returns an element's position among its siblings, and sibling-count() returns the total number of direct child elements, including the element on which it is used — both as plain numbers.
I wanted to see what these could do for cases that used to need JavaScript.
Browser support is still uneven — some demos below also lean on the newer if()/style() and relative colour syntax. For the smoothest experience, open the demos in a recent Chrome or Chromium-based browser. Elsewhere, each demo detects support at runtime and shows a plain message instead of a broken layout.
Show n chips with a "view more" count
/* How many chips stay visible before the list collapses. */ :root { --visible: 5; } /* * Registered so the values below compute to real integers. Unregistered custom * properties stay raw tokens, and a style() query can only compare computed * values — registering is what makes the if() rules work. */ @property --overflowing { syntax: '<integer>'; inherits: false; initial-value: 0; } @property --overflow-count { syntax: '<integer>'; inherits: false; initial-value: 0; } .chips li { /* 0 while the chip fits, 1 once it is past the cut-off. */ --overflowing: clamp(0, sibling-index() - var(--visible), 1); display: inline-flex; /* * No selector can read sibling-index(), so the count becomes a number and * if() turns that number back into a keyword. */ display: if(style(--overflowing: 1): none; else: inline-flex); /* Each chip enters a beat after the one before it. */ animation-delay: calc((sibling-index() - 1) * 35ms); } /* The toggle: counts what is hidden, and hides itself when nothing is. */ .chips li.more { /* sibling-count() includes this <li>, hence the extra - 1. */ --overflow-count: max(0, sibling-count() - var(--visible) - 1); display: inline-flex; display: if(style(--overflow-count: 0): none; else: inline-flex); counter-reset: overflow var(--overflow-count); } .chips li.more .show-more::before { content: '+' counter(overflow) ' more'; }
Try changing the --visible CSS variable and watch the component respond — including the "n more" count — with zero JavaScript involved.
JS here is only used to add new chips.
Notice it also uses the counter() CSS function to display the count. The difference: counter() returns a string, while sibling-count() returns a number — better suited when you want to compute with it rather than just display it.
Auto-generate chart colors
/* * Registered so the palette can be transitioned when the values change, and so * --saturation stays a real percentage rather than a raw token. */ @property --hue-base { syntax: '<number>'; inherits: true; initial-value: 205; } @property --saturation { syntax: '<percentage>'; inherits: true; initial-value: 55%; } .chart { /* The knobs the Randomize button turns. Everything below inherits them. */ --hue-base: 205; --hue-step: 55; --saturation: 55%; transition: --hue-base 0.5s var(--ease), --saturation 0.5s var(--ease); } .group { /* Hue comes from the group's position among its sibling groups. */ color: hsl(calc(var(--hue-base) + (sibling-index() - 1) * var(--hue-step)) var(--saturation) 45%); /* The axis label counts itself, so it can never disagree with the bars. */ counter-reset: group sibling-index(); &::after { content: 'G' counter(group); } } /* * sibling-index() restarts inside each group, so this ramps lightness per * series. Piping a sibling-index() value through a custom property and into a * descendant froze at the first group's value — setting the inherited `color` * above and deriving from currentColor here sidesteps that. */ .bar { background: hsl(from currentcolor h s calc(l + (sibling-index() - 1) * 20)); } /* Same two formulas as .group — the legend cannot drift out of sync. */ .chart__legend li { color: hsl(calc(var(--hue-base) + (sibling-index() - 1) * var(--hue-step)) var(--saturation) 45%); counter-reset: group sibling-index(); &::after { content: 'G' counter(group); } }
Normally you'd pass an array of colors to your charting library, which assigns them by index — duplicating your theme in JS. With sibling-index(), that color logic can live entirely in CSS instead.
A table that grades on a curve
/* * A grade on a curve is a share of the class, not a score threshold. Which band * a row lands in depends on how many rows the table holds — so no selector can * decide it. The same third row is an A in a class of twenty and a B in a class * of eight, from identical markup. */ /* * Registered so the tier computes to a real integer: style() compares computed * values, and an unregistered property would still be the literal text * "round(down, …)". inherits: false keeps the positional value on the row — * piping one down through an inherited custom property freezes it at the first * element's value. */ @property --tier { syntax: '<integer>'; inherits: false; initial-value: 0; } /* * The row works out a number; this renders it as a letter. system: fixed maps * the first symbol to 1, which is what the + 1 below is for. */ @counter-style grades { system: fixed; symbols: 'A' 'B' 'C' 'D' 'E'; suffix: ''; fallback: decimal; } .curve { /* The whole grading policy, in one token. */ --bands: 4; --tint-a: color-mix(in srgb, var(--color-success, #16a34a) 20%, transparent); --tint-b: color-mix(in srgb, var(--color-info, #0ea5e9) 16%, transparent); --tint-c: color-mix(in srgb, var(--color-warning, #f59e0b) 16%, transparent); --tint-d: color-mix(in srgb, var(--color-error, #ef4444) 16%, transparent); --tint-e: color-mix(in srgb, var(--color-error, #ef4444) 28%, transparent); } tbody tr { /* * 0 for the top band of rows, --bands - 1 for the bottom, at any row count. * The largest value this can take is (n - 1) * bands / n, which stays below * bands for every n — so the result is already in range and needs no clamp, * and a one-row table divides by one rather than by zero. */ --tier: round(down, (sibling-index() - 1) * var(--bands) / sibling-count(), 1); /* The fallback: a browser without if() drops the line below and stays plain. */ background-color: transparent; /* prettier-ignore */ background-color: if( style(--tier: 0): var(--tint-a); style(--tier: 1): var(--tint-b); style(--tier: 2): var(--tint-c); style(--tier: 3): var(--tint-d); else: var(--tint-e) ); /* Nothing in the markup says "B". The row counts its way to the letter. */ counter-reset: grade calc(var(--tier) + 1); transition: background-color 0.4s var(--ease); } .grade::before { content: counter(grade, grades); }
This one shows that even semantic coloring — grading each cell relative to its siblings — can be done in pure CSS.