Less Mixin Guards and Recursion: Generating Grid Utilities at Build Time
Less has no native loops, but recursive mixins with guards can generate repetitive utility classes at build time. Learn the pattern, its costs, and when to pair it with CSS custom properties.
07 Aug 2026, 23:08 UTC

The repetitive class problem
If you maintain a design system by hand, you have probably written the same utility class a dozen times: .col-1 through .col-12, or .mt-1 through .mt-8. When the base spacing scale changes, every rule needs an edit. That is tedious and easy to get wrong. Less gives you a way to generate these rules at compile time, but it does not expose a native loop. Instead, you combine mixin guards with recursion to fake one.
Recursive mixins as build-time loops
In Less, a mixin can carry a guard—a condition attached with when that decides whether the mixin body applies. Mixins with the same name form an overload set, and Less matches them by name and the number of arguments they accept. To simulate a loop, you write two mixins with the same name: one guarded to run while a counter is within range, and one guarded to stop when the counter exceeds it. The first mixin emits a rule and then calls itself with an incremented counter.
Because Less evaluates everything ahead of time, the recursion happens during compilation. The output is plain static CSS with no trace of the loop.
Worked example: grid column utilities
Suppose you want percentage widths for a flexible grid. The following mixin generates .col-1 through .col-n:
.grid-cols(@n, @i: 1) when (@i =< @n) {
.col-@{i} {
width: (@i * 100% / @n);
}
.grid-cols(@n, @i + 1);
}
.grid-cols(4);
The guard when (@i =< @n) keeps the mixin alive while the counter @i is less than or equal to the target @n. Inside the body, interpolation @{i} builds the class name, and the final line calls the same mixin with @i + 1. When @i finally exceeds @n, no guarded mixin matches and the recursion stops.
To run this, save the snippet to input.less and compile it from your terminal:
npx lessc@4 input.less output.css
Open output.css and confirm that the selectors .col-1 through .col-4 appear with widths of 25%, 50%, 75%, and 100%. If they do not, check your Less version first: guard operators are version-sensitive. Less traditionally accepts =< and >=; confirm whether your installed version also parses <= before rewriting the guard.
Lazy variables and scoping surprises
Less variables are lazily evaluated. That means a variable is not resolved when it is first referenced, but when it is finally used. Combined with the rule that the last definition in a scope wins, this produces behavior that can feel backwards:
@theme: dark;
.button { color: @theme; }
@theme: light;
The .button rule will use light, not dark, because the second definition overwrites the first before the value is emitted. This is useful when you want a downstream file to override a default, but it is confusing during debugging because the source order in the file does not match the order of evaluation.
Scoping adds another wrinkle: variables are pulled from outer scopes into nested rulesets, but they are never pushed inward. Redefining a variable inside a nested block does not affect the parent scope.
Trade-offs and practical limits
Recursive mixins are clever, but they are not free. Here is how they compare to writing utilities by hand or using CSS custom properties:
| Approach | When values change | Debugging | Build cost |
|---|---|---|---|
| Recursive mixin utilities | Build time only | Harder; errors reference the mixin, not the output line | Grows with the number of generated classes |
| Hand-written CSS | Manual edits | Direct | None |
| CSS custom properties | Runtime | Direct | Negligible |
Because the compiled CSS is frozen, generated utilities cannot react to runtime state such as user preferences or container size. If you need that flexibility, define the variable part as a CSS custom property and let the browser resolve it.
There are also compiler-specific limits. Recursive mixins have no break or continue, so every iteration must fall through to the next call. Generating hundreds of utilities can measurably slow compilation, so time your build before committing to a large token set. Guard overload sets match on name and arity—the number of arguments—which means adding or removing a parameter during a refactor can silently change which mixin applies. Finally, inline JavaScript in Less files was deprecated and is disabled by default in Less 4; avoid backtick JavaScript and use plain Less constructs or an approved plugin instead.
Verify before you trust it
Start small. Compile a file that generates only a handful of classes, then diff the output against hand-written CSS for the same utilities to check rule ordering and specificity. Run lessc --version and confirm the guard operator syntax against the documentation for that version. If you scale up, time compilation with and without the recursive mixin at a large @n to quantify the cost. Treat the generated file as build output, not source, and keep the recursive mixin in a clearly named partial—an imported Less file—so the next developer knows where the utilities come from.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.