Less Mixins: Default Parameters or Guards for Optional Styles
Default parameters cover most Less mixin needs; guards earn their keep for type checks and feature flags. A comparison table, a button example, and how to verify the compiled CSS.
20 Mar 2026, 19:38 UTC

You want one Less mixin that renders a sensible default button, accepts a partial override such as "same padding, different background," and does not silently emit a rule-set when a caller passes something the mixin cannot use. Less 3.9+ offers two ways to get there: default parameters, or guarded mixins that test arguments before emitting anything. They are not interchangeable, and the difference is most visible when a call does not match.
The decision and the constraints
A parametric mixin is a reusable block of declarations whose arguments are substituted at compile time. Unlike a normal class, a mixin declared with parentheses is never emitted on its own — it is inlined into every selector that calls it. That inlining drives most of the trade-offs here.
- Compiler version. Assume Less 3.9 or later. Undefined-variable handling and guard evaluation have changed across Less major releases, so confirm your version with
lessc --versionbefore relying on edge-case behavior. - No duplicate or empty output. Because mixins are inlined, every call site produces a full copy of the declarations. A selector whose only content is a mixin call that produced nothing is a case worth checking in your compiler's output.
- Partial overrides. Callers should be able to set only
@colorwithout restating@bg. - Unit and type consistency. Guards compare values. Mixed units are a common source of surprising non-matches, so keep units consistent across call sites.
Comparing the supported options
| Approach | Declaration shape | Partial override | When a call does not match | Typical use |
|---|---|---|---|---|
| Default parameters | .button(@bg: #fff; @color: #000) |
Yes, via named arguments | Still emits; defaults fill the gaps | Most component mixins |
| Guarded mixin | .button(@bg) when (iscolor(@bg)) |
Only for arguments the guard accepts | Emits nothing; the call is a silent no-op | Type or unit validation, feature flags |
| Arity overloads | .button() and .button(@bg) |
No | The non-matching arity is simply not selected | Variants with genuinely different shapes |
default() fallback |
.button(@v) when (default()) |
Not applicable | Runs only when no other mixin in the set matched | Catch-all branch in a guarded set |
Where each approach breaks down
Default parameters are the right default choice. Named-argument calls such as .button(@color: #fff) give you partial overrides for free, and the mixin always produces output, so a mistake shows up as visibly wrong CSS rather than as missing CSS. The cost is that a bad value is accepted without complaint: passing 16px as a background compiles to background: 16px.
Guards invert that failure mode. A guard is a condition attached with when; if it evaluates false, the mixin does not match and contributes nothing. That is valuable for compile-time feature flags and for rejecting values of the wrong type, but it means a typo produces no CSS and no error. Use guards when "emit nothing" is the correct behavior, not as a general validation layer.
Arity overloads are the cleanest way to express genuinely different variants, but they do not help with partial overrides, because each overload is a separate declaration set.
A concrete implementation
The example below assumes Less 3.9+ and a file named styles.less. Note the semicolons between arguments: when any argument value contains a comma — for example rgba(0, 0, 0, 0.2) — commas as separators become ambiguous, so semicolons are the safer convention throughout.
// styles.less
.button(@bg: #ffffff; @color: #000000; @pad: 8px 16px) {
background: @bg;
color: @color;
padding: @pad;
border: 1px solid darken(@bg, 10%);
}
.plain { .button(); }
.primary { .button(#008000; #ffffff); }
.ghost { .button(@color: #ffffff; @bg: #333333); }
The same file can carry a guarded mixin for a different job — a compile-time feature flag. The rule is emitted only when the flag is true, so the guard is doing something defaults cannot do.
// Feature-flag guard: emitted only when @shadows is true
@shadows: true;
.card(@shadow: 0 1px 2px rgba(0, 0, 0, 0.2)) when (@shadows = true) {
box-shadow: @shadow;
}
// Type guard: comma means OR, `and` means AND
.spacing(@v) when (ispixel(@v)), (isem(@v)) {
padding: @v;
}
Validating the compiled output
Run the compiler from the directory containing the file, as a normal user with write permission to that directory. Either a global install or a one-off invocation works:
lessc styles.less styles.css
# or, without a global install:
npx lessc styles.less styles.css
This writes styles.css next to the source; the file is disposable and can be deleted or regenerated at any time. Then check the output rather than trusting the source:
- Count the rule-sets. For the button example you should find exactly three selectors —
.plain,.primary,.ghost— in source order. - Confirm that no
.buttonblock appears at all. Parametric mixins are inlined, never emitted. - Confirm the overrides landed:
.primaryshould carrybackground: #008000, and.ghostshould carrybackground: #333333withcolor: #ffffff. - Add a deliberately failing guarded call, such as
.spacing(2rem), and check whether your compiler leaves an empty rule-set behind for that selector. Behavior here is worth confirming on your installed version rather than assuming.
The example above is illustrative and has not been executed here; compile it locally and compare against your own expected output.
Limitations and things to verify
- Silent no-ops. A failed guard produces no CSS and no error message. If you use guards, add a review step or a lint pass over the compiled CSS, because the compiler will not tell you a call was dropped.
- Inlining cost. Twenty call sites mean twenty copies of the declarations. If that matters, a shared class or
:extendproduces smaller output, at the cost of a different override model. - Undefined variables. Do not assume an undefined variable is silently treated as an empty string. This behavior has changed across Less releases; test it against your installed compiler instead of relying on a remembered rule.
- Unit comparisons in guards. Whether
0and0pxcompare equal in a guard is exactly the kind of edge case to check with a two-line test file. Keeping units consistent across call sites avoids the question entirely. default()semantics. The fallback branch is documented as running when no other mixin in the set matched. Verify the exact behavior on your version before depending on it in a shared library.
Practical rule of thumb: reach for default parameters first, because they fail loudly and support partial overrides. Add a guard only when "emit nothing" is the intended outcome — a feature flag, or a type check whose failure is meaningful.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.