CSS Sibling Functions Remove Repeated UI JavaScript


CSS sibling-index() and sibling-count() let a component read its position and the size of its sibling set directly in CSS. That removes a surprising amount of presentation-only JavaScript: no loop-assigned --index variables, no React props passed only to stagger an animation, and no fragile :nth-child() rule ladder. The functions are newly Baseline 2026, so they are now a practical progressive enhancement for lists, galleries, and repeated UI.

What do CSS sibling functions solve?

Sibling functions solve the “same component, different position” problem. A repeated item often needs a value based on its order, or on how many peers it has. Historically, teams handled that with JavaScript, server-rendered inline styles, or a hard-coded sequence of selectors.

CSS now supplies two numeric functions:

They return numbers, not strings. That is the important detail: the result can participate in calc(), colors, durations, transforms, grid placement, and custom-property calculations.

For example, the third card inside a card list gets sibling-index() equal to 3. If that list contains eight cards, every card gets sibling-count() equal to 8. Text nodes and comments do not affect the result, and the count is based on the DOM’s element children, not on visual order.

This is a small addition with a large design-system payoff. It keeps a component’s repeated-state logic in the cascade, where the visual rule belongs.

How do sibling-index() and sibling-count() work?

Both functions take no arguments and are evaluated for the element being styled. Their values are one-based, like :nth-child(), rather than zero-based like a JavaScript array.

css
.timeline-item {
  --position: sibling-index();
  --total: sibling-count();
}

The values are live with the DOM. Add a card, remove a card, or reorder items and the browser recalculates the dependent styles. There is no cleanup effect and no second render just to repair a list of CSS variables.

That distinction is useful in React and other component frameworks. Passing index is still correct when the number affects data, accessibility text, keys, or business behavior. It is unnecessary when it exists only to make the seventh tile animate later than the sixth.

The functions are defined in the CSS Values and Units Level 5 editor’s draft. As with any modern feature, the spec remains the source of the precise behavior, while the MDN sibling-index() reference is a useful implementation-oriented guide.

When should a team use sibling functions instead of JavaScript?

Use sibling functions when the result controls presentation and can be derived solely from DOM sibling order. Good candidates include:

  • entrance-animation delays
  • equal distribution of colors or rotation angles
  • decorative position markers in timelines
  • progress-like visuals for known repeated items
  • responsive gallery emphasis
  • index labels generated with CSS

Do not use them as a substitute for application state. A multi-step checkout, a sortable data grid with an announced rank, or an item whose position must be exposed to assistive technology still needs semantic markup and, often, application logic. CSS can render a sequence, but it should not become the only source of a meaning users need to hear or act upon.

That boundary mirrors the appeal of scheduler.postTask(): put work in the platform layer only when the platform has enough context to do it correctly. Sibling functions know structural order. They do not know product intent.

How can you create staggered animations without index props?

Staggering repeated items is the clearest win. The old React pattern usually looks harmless:

jsx
{items.map((item, index) => (
  <article
    className="card"
    style={{ '--delay': `${index * 70}ms` }}
    key={item.id}
  >
    {item.title}
  </article>
))}

It works, but it mixes a display concern into rendering. With sibling functions, the markup can describe data and the stylesheet can own timing:

css
@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: reveal 420ms both ease-out;
    animation-delay: calc((sibling-index() - 1) * 70ms);
  }
}

@keyframes reveal {
  from { opacity: 0; transform: translateY(0.75rem); }
  to { opacity: 1; transform: translateY(0); }
}

Subtracting one makes the first item start at 0ms. The reduced-motion condition matters. A stagger that looks polished to one visitor can feel slow or distracting to another, and moving the math to CSS does not remove the accessibility obligation.

For content that arrives asynchronously, this also avoids a class of hydration and transition oddities. The browser computes the correct delay from the final sibling structure. If an item is removed, the sequence closes automatically.

Can sibling functions distribute colors and layout values?

Yes. Combining position and count gives each item a proportion of the whole. A status list can spread its hue around the color wheel without manually assigned modifiers:

css
.status-list > li {
  --step: calc(360deg / sibling-count());
  --hue: calc(var(--step) * sibling-index());

  border-inline-start: 4px solid hsl(var(--hue) 65% 45%);
  background: hsl(var(--hue) 70% 96%);
}

This is most effective for decoration, category differentiation, or data visualizations where the color itself is not the only carrier of meaning. Preserve labels, icons, or patterns for status and error states.

A related pattern is a fan of avatars or cards:

css
.avatar-stack > * {
  --center: calc((sibling-count() + 1) / 2);
  --offset: calc(sibling-index() - var(--center));
  transform: translateX(calc(var(--offset) * -8px));
  z-index: sibling-index();
}

The math responds to a two-person stack and a ten-person stack without changing a selector. Keep an eye on hit targets and reading order, though. A visual fan should not make controls difficult to select.

CSS calculations like these are part of the same shift behind CSS if(): more conditional and numeric presentation logic is becoming native CSS, rather than a reason to create a tiny JavaScript utility.

What are the browser-support and fallback considerations?

MDN marks sibling-index() as newly available in Baseline 2026, with availability across current browser versions since August 2026. That makes it suitable for modern-browser products, but it is not an excuse to forget older embedded browsers, enterprise fleets, or long-lived mobile devices.

Start by making the base layout correct without the feature. Then use @supports to add the enhancement:

css
.card { opacity: 1; transform: none; }

@supports (width: calc(sibling-index() * 1px)) {
  @media (prefers-reduced-motion: no-preference) {
    .card {
      animation: reveal 420ms both ease-out;
      animation-delay: calc((sibling-index() - 1) * 70ms);
    }
  }
}

For a decorative color sequence, a single neutral fallback color is fine. For a required layout algorithm, use an established layout first, such as Grid or Flexbox, and treat sibling-derived sizing as optional polish. Do not rely on an unsupported calculation producing a reasonable default.

Testing should include dynamic insertions, filtered lists, drag-and-drop reordering, and empty or one-item states. Also verify that visual reordering has not drifted from keyboard navigation order. The functions read DOM sibling order, which is usually exactly what you want, but CSS order can make the screen tell a different story from the document.

How do sibling functions fit into a component system?

The best adoption path is intentionally boring:

  1. Identify a repeated component where JavaScript supplies an index only for styles.
  2. Move the calculation into a component stylesheet using a custom property or direct calc().
  3. Keep the unenhanced state readable and usable.
  4. Add an @supports block if your audience includes browsers older than the Baseline window.
  5. Remove the now-unused prop, inline style, or selector ladder.

Avoid publishing --item-index as a permanent public component API when CSS can derive it. APIs are commitments, and presentation-only props often leak through a codebase long after their initial purpose is forgotten.

This does not replace every selector. :nth-child() remains excellent for a couple of discrete rules, such as making the first card featured. Counters remain better for generated textual numbering. The new functions earn their place when a numeric value needs to flow through a calculation.

The platform is steadily taking on jobs that previously required framework glue. The View Transition API handles continuity between visual states, while sibling functions handle a local structural fact. Neither eliminates design work, but both let teams express it closer to the browser’s rendering model.

One final implementation tip: inspect the computed value in DevTools before reaching for more elaborate math. A temporary content value on a pseudo-element or a custom property can quickly reveal whether a component’s DOM structure matches the structure you assumed. This is especially helpful when slots, wrappers, or conditional elements are involved. The browser is deterministic, but the actual sibling set is the source of truth.

FAQ

Are CSS sibling functions ready for production?

They are newly Baseline 2026, so they work in current browser releases but can be absent from older devices. Use them for progressive enhancement or verify your own browser-support requirements before making them essential.

Does sibling-index() start at zero?

No. It starts at 1, matching CSS positional selectors such as :nth-child(). Subtract one when you need a zero-based animation delay or offset.

Do text nodes affect sibling-count()?

No. The functions operate on element siblings. Whitespace and comment nodes do not change the index or count.

Should I replace all list indexes in React with CSS?

No. Keep indexes or explicit data when they affect semantics, state, keys, accessibility, or business behavior. Replace them when they exist solely to calculate a visual value.

Frequently Asked Questions

Are CSS sibling functions ready for production?

They are newly Baseline 2026 and work in current browsers, but use progressive enhancement where older devices matter.

Does sibling-index() start at zero?

No. It starts at 1, like :nth-child().

Should React indexes be replaced everywhere?

No. Replace them only when they exist solely for presentation, not data, semantics, keys, or accessibility.