Skip to main content

Dynamic Selector Logic in CSS: Rebuilding :nth-child() with Custom Variables and Inline Conditionals

The CSS pseudo-class selector :nth-child() has long served as a fundamental tool for applying styles based on an element’s structural position within its parent container. However, standard CSS specification rules dictate that selector arguments are static. Web developers cannot directly bind CSS custom properties—commonly referred to as CSS variables—inside structural selectors. A declaration such as :nth-child(var(--A)*n + var(--B)) is fundamentally invalid because selector matching occurs before standard variable resolution takes place.

To alter the step or offset parameters of an :nth-child() selector at runtime, developers historically had to rewrite selector rules in their stylesheets or manipulate DOM classes using JavaScript. With the emergence of advanced CSS features—specifically inline conditional functions like if(), container style queries via style(), structural functions like sibling-index(), and extended mathematical functions such as mod() and sign()—it is now possible to construct dynamic selector logic directly inside property declarations.

While replacing a native selector with computational conditional logic may initially seem unnecessary, examining this re-implementation reveals how upcoming specification features allow stylesheets to perform dynamic, structural evaluations without external scripts.


The Core Constraints of Native Selector Parameters

In standard stylesheet architecture, structural pseudo-classes evaluate an element’s position relative to its siblings. The standard syntax supports static integers, keyword values like even or odd, and algebraic expressions in the form of An + B, where A represents the step interval and B represents the starting offset.

/* Standard static selector evaluation */
div:nth-child(5n + 8) 
  background: red;

If a design system requires changing the interval value 5 or offset value 8 based on viewport context, container size, or user interaction, the stylesheet must declare distinct selector rules for every permutation. Standard CSS custom properties cannot be injected directly into selector definitions:

/* Invalid CSS syntax: Custom properties cannot be evaluated in selectors */
.container 
  --A: 5;
  --B: 8;


.container > div:nth-child(var(--A)*n + var(--B)) 
  background: red;

By shifting the structural calculation from the selector stage into the property value declaration stage, developers can bypass this restriction. Using a basic HTML structure containing sibling elements within a parent container, custom variables defined at the container level can dynamically drive element-level styles.

<div class="container">
  <div></div>
  <div></div>
  <div></div>
  <!-- Additional sibling elements -->
</div>
.container 
  --A: 5;
  --B: 8;


.container > div 
  /* Dynamic property value resolution replacing static selector rules */
  background: /* Conditional computation based on --A and --B */;

Implementing Target Selection for Single Elements: :nth-child(B)

The simplest application of positional targeting involves selecting a single element at a specific index, traditionally achieved using :nth-child(B). For instance, targeting the fifth child element requires matching an element whose structural index is exactly five.

Using modern CSS capabilities, this target selection can be represented by comparing an element’s runtime sibling index against a container variable. The sibling-index() function returns an element’s 1-based sequential position among its siblings, while the if() function evaluates conditional criteria inside property values.

.container 
  --B: 5;


.container div 
  background: if(style(sibling-index() = --B): red); 

In this implementation, the if() function checks a style query condition established by style(). If the numerical result of sibling-index() equals the value stored in custom property --B, the background property resolves to red. If the statement evaluates to false, the rule yields no styling declaration, matching the behavior of an un-matched pseudo-class selector.

Re-Implementing :nth-child() With Modern CSS | CSS-Tricks

Fallback conditions can be declared explicitly using an else clause within the inline conditional structure:

.container 
  --B: 5;


.container div 
  background: if(style(sibling-index() = --B): red; else: lightblue); 

The internal logic of the conditional function relies on variable comparisons inside style(). Syntax rules permit multiple equivalent expressions for evaluating the structural index against container parameters:

.container div 
  background: if(style(sibling-index() = --B): red);
  background: if(style(sibling-index() = var(--B)): red);
  background: if(style(var(--B) = sibling-index()): red); 
  background: if(style(--B = sibling-index()): red); 

This approach allows a single CSS property declaration to apply across multiple style properties, dynamically adjusting targeted elements when the value of --B changes:

.container 
  --B: 5;


.container div 
  background: if(style(sibling-index() = --B): red; else: lightblue);
  color: if(style(sibling-index() = --B): white);

Defining Index Thresholds: :nth-child(n + B) and :nth-child(-n + B)

Beyond single-element selection, :nth-child() expressions frequently define index boundaries. The notation :nth-child(n + B) targets all elements starting from index B onward to the end of the container.

To recreate this behavior dynamically, the comparison operator within the container style query is changed from an equality operator to a greater-than-or-equal comparison (>=).

.container 
  --B: 5;


.container div 
  background: if(style(sibling-index() >= --B): red; else: lightblue);

When an element’s structural index reaches or exceeds the threshold value of --B, the conditional statement resolves to true, selecting the element and applying the corresponding styles.

Conversely, targeting elements from the beginning of a container up to a specific upper limit—traditionally declared as :nth-child(-n + B)—requires a less-than-or-equal comparison (<=).

.container 
  --B: 5;


.container div 
  background: if(style(sibling-index() <= --B): red; else: lightblue);

By combining lower and upper index boundaries, developers can construct range-based selections. In standard CSS, selecting elements within an inclusive numeric range [X, Y] requires chaining two pseudo-class selectors:

/* Traditional static range selection */
div:nth-child(n + X):nth-child(-n + Y) 
  background: red;

With modern style query conditions, complex range expressions can be written as a single compound comparison:

Re-Implementing :nth-child() With Modern CSS | CSS-Tricks
.container 
  --X: 3;
  --Y: 8;


.container div 
  background: if(style(--X <= sibling-index() <= --Y): red);

This syntax provides an intuitive representation of range boundaries while remaining fully connected to dynamic variables.


Evaluating Step Intervals: :nth-child(An)

Selecting elements based on repeating numerical intervals—such as every third element (:nth-child(3n))—requires verifying that an element’s index is an exact multiple of step size A. In computational mathematics, this check is performed using a modulus operation, which calculates the remainder of a division operation.

CSS provides mathematical modulus functionality via the mod() function. If mod(index, A) evaluates to 0, the index is a multiple of A, indicating that the element satisfies the step condition.

.container 
  --A: 3;


.container div 
  background: if(style(mod(sibling-index(), var(--A)) = 0): red);

This logic accurately highlights the 3rd, 6th, 9th, and subsequent elements in a layout sequence. Special cases are handled naturally: setting --A to 1 targets every element (:nth-child(n)), while setting --A to 2 selects even-indexed elements (:nth-child(2n)).

Handling Negative Step Values

A mathematical edge case arises when parameter A holds a negative value. Under standard CSS specifications, a negative step size such as :nth-child(-3n) selects zero elements because DOM structural indexes are positive integers starting at 1.

However, raw mathematical evaluation of mod(index, -3) can still return 0, which would incorrectly highlight elements if left unmanaged. To maintain strict alignment with standard CSS selector behavior, the style query must combine the modulus condition with a secondary check verifying that --A is strictly positive.

.container 
  --A: 3;


.container div 
  background: 
    if(style((mod(sibling-index(), var(--A)) = 0) and (var(--A) > 0)): red);

By using the logical and operator inside the style() query, the property evaluation guards against unintended mathematical matches when negative step parameters are supplied.


Constructing the Universal Expression: :nth-child(An + B)

Combining step intervals (A) and positional offsets (B) forms the general equation for structural selection:

$$textindex = A cdot n + B$$

Re-Implementing :nth-child() With Modern CSS | CSS-Tricks

To determine whether a specific element with a known sibling-index() matches this algebraic rule, the equation is solved for variable $n$:

$$n = fractextindex – BA$$

For an element to be selected, calculated value $n$ must satisfy two fundamental criteria:

  1. $n$ must be a non-negative value ($n ge 0$).
  2. $n$ must be a whole integer, meaning $(textindex – B)$ is evenly divisible by $A$.

Translating these mathematical constraints directly into conditional CSS rules yields an expression that accounts for both integrality and positivity:

.container 
  --A: 3;
  --B: 5;


.container div 
  background: 
    if(style( (mod(sibling-index() - var(--B), var(--A)) = 0) 
      and (calc((sibling-index() - var(--B))/var(--A)) >= 0) 
    ): red);

Resolving Division by Zero Edge Cases

While the above conditional statement handles non-zero values of --A, setting --A to 0 introduces a division-by-zero error. In CSS mathematical calculations, dividing by zero or passing zero as the denominator of mod() results in an invalid NaN (Not a Number) value, causing the conditional query to fail.

When parameter A is 0, the generic algebraic formula :nth-child(0n + B) simplifies to :nth-child(B), targeting only the single element at index B.

To build a fully robust universal system that accounts for --A = 0 without producing invalid calculations, intermediate computed variables can be introduced. These helper variables normalize the logic using mathematical functions like sign() alongside conditional branches.

.container 
  --A: 3;
  --B: 5;


.container div 
  --n: calc((sibling-index() - var(--B)) / var(--A));
  --g: if(style(--A = 0):
         calc(sign(sibling-index() - var(--B)) + 1);
       else:
         calc(sign(var(--n) + 1) + mod(var(--n), 1))
       );
  background: if(style(--g = 1): red);

In this implementation:

  • --n computes the theoretical step multiplier $n$.
  • When --A equals 0, helper flag --g relies on sign(sibling-index() - --B) to check for an exact positional match.
  • When --A is non-zero, --g checks both the remainder mod(var(--n), 1) to confirm $n$ is an integer and the sign offset sign(var(--n) + 1) to ensure $n ge 0$.
  • If computed flag --g resolves to 1, the target condition is satisfied, and the visual background property updates accordingly.

Through this calculated flag mechanism, any standard selector variation—including :nth-child(7), :nth-child(n + 10), :nth-child(-n + 10), or :nth-child(3n)—can be produced dynamically simply by updating custom properties --A and --B on the parent container.

Re-Implementing :nth-child() With Modern CSS | CSS-Tricks

Extending Functional Logic to Reverse Orders

The techniques used to re-implement :nth-child() extend naturally to reverse structural selectors like :nth-last-child().

While :nth-child() counts elements sequentially from the beginning of a parent container, :nth-last-child() counts elements backward from the end. To perform dynamic reverse targeting, calculations can incorporate the sibling-count() function, which returns the total number of sibling elements within the parent container.

By subtracting sibling-index() from sibling-count(), an element’s reverse index can be derived:

$$textreverse-index = textsibling-count() – textsibling-index() + 1$$

Replacing sibling-index() with this reverse positional calculation allows dynamic parameters --A and --B to control backward-counting selector logic without structural HTML modifications.

These capabilities highlight a broader shift in native web capabilities. By integrating mathematical functions, container style queries, and DOM structural functions, modern CSS can process dynamic conditional logic directly within declarative stylesheets.

Basiran

Author at DesignEnt.

Flash News