Parameterized Mixins in Stylus: Reusable CSS Rules Without the Bloat
Stylus mixins inline their body at every call site, so they shine for parameterized rules and hurt when overused. Syntax, the mixin-vs-extend decision, and verification.
05 Oct 2026, 23:27 UTC

If you keep copying the same three-line flexbox centering block or vendor-prefixed rule into every component, a parameterized Stylus mixin is the fix: define the rule once with arguments, call it with different values, and let the compiler inline the result. The catch is that mixins literally copy their body into every call site, so the decision of when to use one — versus @extend or a plain utility class — matters as much as the syntax.
Defining and calling a mixin
In Stylus, a mixin is just a function-like block. You define it with a name and parameter list, and you invoke it by writing that name as if it were a property. There is no @mixin or @include keyword — that is Sass syntax and does not apply here.
// mixins.styl
flex-center(direction = row)
display flex
flex-direction direction
justify-content center
align-items center
.button
flex-center()
padding 0.5rem 1rem
.sidebar
flex-center(column)
Compiling this with stylus mixins.styl (run in the project directory; requires the stylus package installed globally via npm i -g stylus or locally with npx stylus) produces:
.button {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0.5rem 1rem;
}
.sidebar {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
Two things to notice. First, direction = row gives the parameter a default, so flex-center() works with no argument. Second, the mixin body is duplicated verbatim at each call site — the parameter is substituted, then the CSS is emitted inline. That substitution is the whole mechanism; there is no runtime indirection.
Mixin versus @extend: the real engineering decision
Because mixins duplicate output, they are the wrong tool when the rule is identical everywhere. If ten components share the exact same display: flex block, @extend produces one grouped selector instead of ten copies:
$centered
display flex
justify-content center
align-items center
.card
@extend $centered
.modal
@extend $centered
The placeholder selector $centered emits nothing on its own; the compiler merges .card and .modal into a single rule. The trade-off: @extend cannot take parameters, and it can produce surprising selector merging inside media queries. The practical rule of thumb:
- Same output everywhere — use
@extendwith a placeholder. - Output varies by argument (colors, sizes, directions) — use a mixin.
- One-off rule — just write the CSS; abstraction overhead is not worth it below two or three uses.
Limits and common mistakes
Parameters are untyped. Stylus will happily substitute whatever you pass. Calling flex-center(12px) compiles to flex-direction: 12px, which is invalid CSS that the browser silently drops. The compiler will not warn you. For mixins with constrained values, guard with a conditional:
flex-center(direction = row)
unless direction in (row column)
error('flex-center: direction must be row or column')
display flex
flex-direction direction
Output size grows linearly with call sites. A 10-property mixin called 40 times adds 400 declarations to your stylesheet. Check the compiled output size before and after introducing a heavily used mixin; if it balloons, move the static parts into an extended placeholder and keep only the parameterized parts in the mixin.
Mixins inherit the calling context. A mixin invoked inside .panel emits its rules under .panel. That is useful for component modularity, but it means a mixin that itself contains nested selectors can generate deeper specificity than you expect, making later overrides painful. Keep mixin-generated selectors flat.
Verifying the result
Do not trust the abstraction — inspect the output. Compile and read the CSS:
npx stylus mixins.styl --printThe --print flag writes to stdout instead of a file, which is handy for a quick check. Confirm that (1) the parameter was substituted where you expected, (2) no stray default value leaked into a call that passed an explicit argument, and (3) the declaration count matches your mental model of the duplication. For a CI guard, compile in a pipeline step and fail on any Stylus error; the error() guard shown above turns bad arguments into build failures rather than silent browser no-ops.
Assumptions: examples target Stylus 0.5x–0.6x syntax (indentation-based, no semicolons required). The core mixin behavior described here has been stable for years, but verify against your installed version with stylus --version if you are on an older release.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.