Less Mixin Guards: Compile-Time Pattern Matching for Design Systems
Less mixin guards use the `when` keyword to create compile-time pattern matching for conditional styling — think functional pattern matching for CSS. This post walks through a practical button-variant pattern, the string-token trap, and why guard ordering matters.
25 Aug 2026, 17:47 UTC

The Problem: Conditional Styling Without Runtime Cost
Design systems need to express variants — primary buttons, danger alerts, dark-mode cards — without shipping conditional logic to the browser. Sass solves this with @if/@else inside mixins. Less takes a different approach: mixin guards using the when keyword. Guards let you define multiple overloads of the same mixin name, each with a boolean condition. The compiler picks the first matching overload at compile time, emitting plain CSS with zero runtime overhead.
How Guards Work
A guarded mixin looks like this:
.button-variant(@bg, @text) when (iscolor(@bg)) and (lightness(@bg) > 50%) {
background: @bg;
color: @text;
border: 1px solid darken(@bg, 10%);
}
.button-variant(@bg, @text) when (iscolor(@bg)) and (lightness(@bg) <= 50%) {
background: @bg;
color: @text;
border: 1px solid lighten(@bg, 10%);
}
Call .button-variant(#333, #fff) and the second overload wins. Call .button-variant(#eee, #333) and the first wins. The when clause accepts type-checking functions (iscolor(), isnumber(), isstring(), ispixel(), ispercentage(), isem(), isunit()), comparison operators (>, >=, <, <=, =, !=), and logical keywords (and, or, not). Guards evaluate in definition order — first match wins.
A Practical Pattern: Semantic Button Variants
Design tokens often arrive as semantic names, not raw colors. Guards let you dispatch on those tokens directly:
// Token definitions (typically imported from a theme file)
@primary: #2d6cdf;
@danger: #dc3545;
@success: #28a745;
// Guarded overloads for each semantic variant
.button-variant(@variant) when (@variant = primary) {
.button-variant(@primary, contrast(@primary));
}
.button-variant(@variant) when (@variant = danger) {
.button-variant(@danger, contrast(@danger));
}
.button-variant(@variant) when (@variant = success) {
.button-variant(@success, contrast(@success));
}
// Fallback for raw color pairs
.button-variant(@bg, @text) when (iscolor(@bg)) and (iscolor(@text)) {
background: @bg;
color: @text;
}
// Catch-all using default()
.button-variant(@_) when (default()) {
background: @primary;
color: contrast(@primary);
}
Usage stays simple: .button-variant(primary), .button-variant(danger), or .button-variant(#custom, #fff). The default() guard acts as an explicit fallback — without it, an unmatched call throws a compilation error. Note that contrast() is a Less built-in that returns black or white based on perceived luminance.
Guard Ordering and the String Token Trap
Because guards are evaluated top-to-bottom, specific checks must precede general ones. Put exact-value guards (@variant = primary) before type checks (iscolor(@bg)). A common pitfall: passing design tokens as strings. iscolor('primary') returns false — 'primary' is a string, not a color. If your token map uses string keys, resolve them before the guard:
@token-map: primary @primary, danger @danger, success @success;
.button-variant(@token) when (iscolor(extract(@token-map, @token))) {
.button-variant(extract(@token-map, @token), contrast(extract(@token-map, @token)));
}
This keeps call sites clean (.button-variant('primary')) while letting guards work on computed colors.
Trade-offs and Limitations
- CSS bloat: Each invoked overload emits its own rule set. If you call 20 distinct guarded variants, you get 20 rule blocks. Audit with
lessc --clean-cssand check gzip size. - No cross-mixin guard sharing: Guards live on individual mixin definitions. You can't extract a guard condition into a reusable variable.
- IDE support is thin: VS Code's Less extension shows guard hover info but can't rename a mixin across all its guarded overloads. Document guard contracts in JSDoc-style comments for team maintainability.
- Plugin purity requirement: Custom functions used in guards must be pure and synchronous. Async plugins break guard evaluation.
Try It in a Small Component
Add a guarded mixin to your next component — a badge, alert, or button variant. Compile with lessc --source-map and inspect the map: each generated rule points back to the specific guard block that produced it. That traceability is the real win: you see exactly which condition fired, without debugging runtime styles. If the output looks clean and the compile stays fast (sub-100ms even with 50+ overloads), you've got a pattern worth scaling across the design system.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.