Reusable Pug Mixins: Attribute Merging, Rest Arguments, and Blocks
Build reusable Pug components with mixins: capture caller attributes with the attributes object, accept variable input with rest arguments, and wrap content with blocks — plus the class-merging and indentation traps to avoid.
14 Aug 2025, 15:54 UTC

If your Pug templates repeat the same form field or card markup with small variations, a mixin is the fix: declare it once with mixin name(args), call it with +name(args), and parameterize the differences. The three features that turn mixins from simple snippets into a real component layer are the attributes object, rest arguments, and block content. This guide shows a worked example of each, then covers the failure modes that bite in practice.
The mechanism: mixins compile into your template function
Pug compiles each template into a JavaScript function at render time. A mixin becomes a nested function inside that compiled output, so calling one adds negligible runtime cost compared with hand‑written markup — the real cost is template compilation, which Express caches for you when NODE_ENV=production. That means you can use mixins liberally for structure without a performance argument against them.
Worked example: a form field component
Create a shared file, views/_mixins.pug, and include it from any template that needs it:
// views/_mixins.pug
mixin field(label, name, type)
.field
label(for=name)= label
input(id=name, name=name, type=type || 'text')&attributes(attributes)
mixin button(...labels)
each text in labels
button.btn(class=attributes.class)= text
mixin panel(title)
section.panel
h2= title
.panel-body
block
Use it in a page template:
// views/signup.pug
include _mixins.pug
+panel('Create account')
form(method='post', action='/signup')
+field('Email', 'email', 'email')(required, placeholder='[contact removed]')
+field('Display name', 'displayName')(maxlength='40')
+button('Sign up')(class='primary')
Three things are happening here:
- Attribute capture. Anything in the parenthesized attribute list at the call site —
required,placeholder,maxlength— lands in the implicitattributesobject. The trailing&attributes(attributes)on theinputsplats those onto the element, so callers can add arbitrary HTML attributes without the mixin knowing about them in advance. - Rest arguments.
mixin button(...labels)collects any number of positional arguments into an array. It is handy for variable content, which is useful for things likemixin nav(...links)where the count genuinely varies. - Block content. The indented lines under
+panel('Create account')are passed into the mixin and rendered where theblockkeyword sits. This is the standard layout‑wrapper pattern: the mixin owns the chrome, the caller owns the content.
Class merging is shallow — concatenate explicitly
The most common mixin bug: you set a default class on the element and the caller also passes class. One silently wins. Merge them yourself:
mixin button(label)
- var cls = 'btn' + (attributes.class ? ' ' + attributes.class : '')
button(class=cls)&attributes(attributes)= label
Note the ordering trap: if you write class=cls and then &attributes(attributes), the splatted class overrides your merged value in the output. A safer pattern is to delete the key after merging:
mixin button(label)
- var cls = 'btn' + (attributes.class ? ' ' + attributes.class : '')
- delete attributes.class
button(class=cls)&attributes(attributes)= label
Verify the behavior by rendering the same call with and without a class attribute and diffing the HTML.
Verifying output locally
You do not need a running app to check a mixin. In a Node script (run with node check.js from your project root, no special permissions needed):
const pug = require('pug');
const html = pug.renderFile('views/signup.pug');
console.log(html);
Inspect the emitted HTML for the merged classes, the splatted attributes, and the panel’s nested block. Also render a hostile value to confirm escaping:
console.log(pug.render('p= userInput', { userInput: '' }));
You should see the angle brackets entity‑encoded. The = operator escapes by default; != does not. Never use != inside a mixin unless the value is guaranteed sanitized — mixins get reused, and a future caller will eventually pass user data.
Limits and common mistakes
- Whitespace is structure. A single wrong indent inside a mixin silently changes the HTML or drops the
blockcontent, and the compiler often will not error. If a mixin renders nothing, check indentation of theblockline first. - Mixins are not data‑bound components. They render once, server‑side. There is no reactivity; anything dynamic after page load needs client JavaScript.
- Includes are textual.
include _mixins.pugpastes the definitions into every template that uses them. Fine for a shared library, but name‑colliding mixins in two included files will conflict. - Compilation caching matters. In Express, set
NODE_ENV=production(or passcache: truetorenderFile) so templates are not recompiled per request. - Project health. Pug’s release cadence has slowed in recent years. Pin an exact version in
package.json, checknpm ls pug, and review the changelog and open security advisories before adopting it for a new project. Behavior described here applies to Pug 3.x; verify against your installed version.
The practical takeaway: keep mixins small, merge classes explicitly, splat attributes last (after deleting keys you consumed), and render a test page through pug.renderFile whenever you change the shared file. That catches indentation and merging mistakes before they reach a route.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.