Less Mixins, Guards, and Pattern Matching vs CSS Custom Properties
Less mixins with pattern matching and guards shine at compile-time structure, but runtime theming belongs to CSS custom properties. Here's how to split the work — and verify it.
18 Nov 2025, 23:35 UTC

You're maintaining a legacy stylesheet written in Less, and someone asks you to add dark-mode theming. Your first instinct is to reach for a mixin with a @theme parameter. Stop there — that instinct is wrong, and understanding why it's wrong teaches you exactly what Less mixins are actually good at: compile-time structure, not runtime values.
The thesis of this piece: parametric mixins with pattern matching and guards are excellent for encoding decisions made at build time (layout patterns, typography rules, contrast-safe color choices), while CSS custom properties are the right tool for anything that must change after the CSS ships. The pragmatic answer is usually a hybrid.
What parametric mixins actually do
A Less mixin is a reusable block of declarations that gets copied — literally inlined — into every place you call it. Parameters make it configurable, with defaults:
.button(@bg: #333, @radius: 4px) {\n background: @bg;\n border-radius: @radius;\n padding: 0.5em 1em;\n}\n\n.save-btn { .button(#06c); }\n.cancel-btn { .button(#888, 2px); }Both selectors end up with their own full copy of the declarations. Nothing about this exists at runtime; lessc resolves everything before the browser ever sees the file.
Pattern matching: dispatch on literal arguments
Less lets several mixins share one name and picks the one whose literal parameters match the call. The special parameter @_ matches anything in that position:
.triangle(up; @size; @color) {\n width: 0; height: 0;\n border-left: @size solid transparent;\n border-right: @size solid transparent;\n border-bottom: @size solid @color;\n}\n.triangle(down; @size; @color) {\n width: 0; height: 0;\n border-left: @size solid transparent;\n border-right: @size solid transparent;\n border-top: @size solid @color;\n}\n\n.tooltip::before { .triangle(up; 6px; #222); }This reads like function overloading and keeps call sites declarative: the direction is data, not a nested if.
Guards: conditional logic at compile time
Guards attach a when expression to a mixin, so the branch is chosen while compiling. The classic use is picking readable text color from a background:
.contrast-text(@bg) when (lightness(@bg) >= 50%) {\n color: #111;\n}\n.contrast-text(@bg) when (lightness(@bg) < 50%) {\n color: #fafafa;\n}\n\n.badge { background: @brand; .contrast-text(@brand); }Two caveats worth documenting in your codebase. First, lightness() is simple HSL math, not perceptual contrast — a 50%-lightness yellow and a 50%-lightness blue do not read the same to human eyes, so test boundary inputs against your real palette. Second, Less variable scoping surprises people: variables are lazily evaluated and the last declaration in scope wins, so a mixin can pick up a value defined after its textual position. Verify behavior on your own files rather than assuming.
The decision: mixin or custom property?
The dividing line is one question: does this value need to change after the CSS is served?
| Need | Right tool | Why |
|---|---|---|
| Dark mode, user themes, JS-driven state | CSS custom properties | --var cascades and updates at runtime |
| Values varying inside media queries | Custom properties | Less variables are frozen at build time |
| Layout patterns, arrow shapes, clearfixes | Less mixins | Structural rules with parameters; no runtime cost |
| Contrast-safe color derivation | Guarded mixin | Decision is known at build time |
The hybrid pattern works because Less happily emits custom property declarations:
:root { --surface: #fff; --ink: #111; }\n[data-theme=\"dark\"] { --surface: #181818; --ink: #eee; }\n\n.card {\n background: var(--surface);\n color: var(--ink);\n .card-chrome(8px); // compile-time structure: radius, shadow, padding\n}Runtime theming flows through the custom properties; the reusable shape of a card stays a mixin.
The trade-off: output bloat
Because mixins duplicate declarations at every call site, heavy use inflates your compiled CSS. How much depends entirely on your stylesheet — measure, don't guess. Compile with and without a mixin-heavy section and compare:
# run in your project, requires less installed (npm i -g less)\nlessc input.less out.css\nwc -c out.cssFor purely static shared styles, Less's :extend pseudo-class merges selectors onto one rule instead of copying declarations, which can shrink output — but it only works when nothing is parameterized. If a value varies per call site, you're back to a mixin (or a custom property).
Acting on this
Audit one of your existing .less files: for every mixin call, ask whether the varying argument is a runtime concern. If yes, convert it to a custom property and keep the mixin for structure only. Then compile and diff the output to confirm the change did what you expected. One honest note on context: Less development has slowed relative to Sass and to native CSS features (nesting, custom properties, color-mix()), so for new code, weigh whether you need a preprocessor at all — but for the large body of existing Less in production, this mixin-versus-custom-property split is the decision that keeps it maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.