Skip to content

LESS Recursive Mixins

LESS has no native for loop. Instead, developers combine a mixin that calls itself with a guard condition that eventually stops the recursion, a technique known as a recursive mixin loop, to generate repeated CSS output.

What Is a Recursive Mixin?

A recursive mixin calls itself from within its own body, decrementing (or incrementing) a counter argument each time. A guard condition checks the counter and stops the recursion once a base case is reached, exactly like recursion in any programming language.

This pattern is most commonly used to generate a series of numbered utility classes, like a 12-column grid system, without writing each class by hand.

.generate-columns(@n, @i: 1) when (@i =< @n) {
  .col-@{i} {
    width: (@i * 100% / @n);
  }
  .generate-columns(@n, @i + 1);
}

.generate-columns(4);

Generates .col-1 through .col-4, each with a proportional width, then stops once @i exceeds @n.

Recursive Mixin Pattern

.loop(@i) when (@i > 0) {
  // work for this iteration
  .loop(@i - 1); // recursive call, moving toward the base case
}
.loop(@i) when (@i =< 0) {
  // base case, stops recursion
}
  • Every recursive mixin needs a guard condition that becomes false eventually, or compilation will loop indefinitely.
  • The recursive call typically increments or decrements a counter argument by exactly one step.
  • A base-case guard (often when (@i =< 0)) is optional but improves clarity even if it does nothing.
  • Recursive mixins are commonly combined with interpolation (@{i}) to generate numbered class names.

Recursive Mixins Cheatsheet

Common recursive-loop patterns for generating repeated CSS.

Use Case Pattern Notes
Countdown loop .loop(@i) when (@i > 0) { .loop(@i - 1); } Counts down to zero
Count-up loop .loop(@i, @max) when (@i =< @max) { .loop(@i + 1, @max); } Counts up to a max
Numbered classes .col-@{i} { } Interpolation names each generated class
Guarded base case when (@i =< 0) Stops the recursion explicitly
Modern alternative each(range(@n), { ... }) each() + range() avoids manual recursion

Tracing a Recursive Mixin Step by Step

Understanding recursion is easier by tracing exactly what happens on each call. Consider a simple countdown mixin that outputs a comment-like custom property for each step.

.count(@i) when (@i > 0) {
  --step-@{i}: reached;
  .count(@i - 1);
}
.count(3);
// Call 1: @i = 3 -> outputs --step-3, calls .count(2)
// Call 2: @i = 2 -> outputs --step-2, calls .count(1)
// Call 3: @i = 1 -> outputs --step-1, calls .count(0)
// Call 4: @i = 0 -> guard (@i > 0) is false, recursion stops

each() and range() as a Modern Alternative

Since LESS 3.7, the built-in each() function combined with range() (LESS 3.9+) can emulate a for loop without manually writing recursive guard logic, and is often easier to read for straightforward numeric loops.

each(range(4), {
  .col-@{value} {
    width: (@value * 25%);
  }
});

This achieves the same result as the recursive .generate-columns() mixin shown earlier, with less boilerplate.

Avoiding Infinite Recursion

Because LESS has no built-in maximum iteration count in older versions, a guard condition that never becomes false will cause the compiler to recurse until it hits a call-stack limit and throws an error.

  • Always move the counter argument strictly toward the base case on every recursive call.
  • Double-check guard operators, =< for less-than-or-equal is easy to typo as <=.
  • Test recursive mixins with a small counter value first before generating a large range.

Common Mistakes

  • Writing <= instead of LESS's actual less-than-or-equal guard operator, =<.
  • Forgetting the base-case guard entirely, causing the compiler to recurse indefinitely and error out.
  • Moving the counter in the wrong direction, causing the guard condition to never become false.
  • Reaching for manual recursion for a simple numeric loop when each(range(n), { ... }) would be simpler.

Key Takeaways

  • Recursive mixins call themselves with a guard condition that eventually stops the recursion.
  • The counter argument must move strictly toward the base case on every call to avoid infinite recursion.
  • Interpolation (@{i}) is commonly used to generate numbered class names from the loop counter.
  • each() combined with range() offers a more concise modern alternative for simple numeric loops.

Pro Tip

Before writing a manual recursive-mixin loop, check whether each(range(n), { ... }) can express the same logic; it's available since LESS 3.9 and is usually easier for teammates to read.