Build Reusable Pug Components with Mixins and Content Blocks
Stop copying button markup across Pug templates. Mixins with parameters and block slots let you define a UI component once and reuse it with different labels, variants, and caller-supplied content.
17 Apr 2026, 12:00 UTC

The problem: copying the same markup
You have a primary button in your login form, your checkout page, and your settings modal. Each copy repeats the same classes, type attribute, and wrapper structure. When the design team changes the button padding or adds an icon slot, you edit every file. That duplication makes small style changes risky and slows down template reviews.
The useful takeaway is that Pug mixins work like reusable components: you define the markup once, accept parameters for values that change, and expose a block slot where the caller can inject its own content. You do not need a frontend framework to stop repeating yourself.
Mixins, parameters, and slots
A Pug mixin is a named template fragment. You define it with mixin, call it with +, and pass arguments the same way you would call a JavaScript function. Parameters can have default values, so a call can omit optional arguments.
Parameters with defaults
This mixin accepts a label and a variant. If no variant is supplied, it defaults to primary.
mixin button(label, variant = "primary")
button.button(class="button button--#{variant}" type="button")
span #{label}
block
The #{label} interpolation is escaped by default, which means angle brackets in user-controlled text become HTML entities. Use !=label only when the markup is trusted; otherwise user input can introduce XSS.
The block slot
A block inside a mixin is a placeholder for caller-supplied content. The caller passes indented content after the mixin call, and Pug renders it at the block position.
+button("Save", "primary")
span.icon.save
In this call, span.icon.save renders inside the button, after the label span. Attributes can also be spread onto the mixin root element with &attributes, which is useful for passing disabled or extra classes from the caller.
Worked example: a button component
Create a file named button.pug in your project. It defines the mixin and immediately demonstrates a call with a content block.
mixin button(label, variant = "primary")
button.button(class="button button--#{variant}" type="button")
span #{label}
block
+button("Save", "primary")
span.icon.save
+button("Cancel", "secondary")&attributes({ disabled: true })
Run the render check from your project root, where pug is installed:
node -e "const pug = require('pug'); console.log(pug.renderFile('button.pug', { pretty: true }));"
Where to run: project root. Permissions: read access to button.pug. Expected check: the printed HTML contains a button element with the saved label, an inner span.icon.save, and a second disabled button. Risk: none; this is a read-only render.
Before relying on version-specific behavior, confirm the installed major version:
node -e "console.log(require('pug/package.json').version)"
Expected check: prints a version such as 3.0.2. Jade-era snippets may not compile under Pug 3, so do not copy older examples without testing.
Limitations and what to check
- Indentation is significant. Mixing spaces and tabs, or letting an editor reformat a file, can change nesting and cause compile errors.
- Mixin
blockcontent is scoped to that call. You cannot query or modify it from outside the mixin. - Unescaped output (
!=or!{}) with user-controlled data can introduce XSS. Prefer#{}and sanitize any HTML you must render raw. - Pug compiles templates to JavaScript functions, so syntax errors appear at compile or render time, not in the browser DOM. Pin the
pugdependency inpackage.jsonbecause major releases can change defaults and supported Node versions.
Verification and closing
To verify the component works, render button.pug and confirm the block content appears inside the button element. Then render the same user-controlled string through #{} and !=; the escaped form should encode angle brackets while the unescaped form does not. Add Pug linting to CI to catch indentation and mixin errors before deployment.
Start with one repeated element, such as a button or form field, convert it to a mixin with a default parameter and a block slot, and replace the copies one at a time. If the rendered HTML matches your existing markup, the refactor is safe.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.