Building Reusable Pug Components with Mixins and Block Placeholders
Learn how to create reusable Pug components with mixins and block placeholders, including definition, usage, validation, and troubleshooting steps.
07 Feb 2026, 08:52 UTC

Repeated UI elements like cards, alerts, and modals often start as copy-pasted markup. Over time the copies drift: one wrapper gains a class, another misses an accessibility attribute, and the template becomes brittle. A Pug mixin with named block placeholders fixes this by defining the outer structure once and letting each call site inject only the content that varies.
Desired outcome
Build a parameterized Pug mixin that encapsulates the markup for a repeated component and exposes named block placeholders for custom inner content. Callers should be able to supply content for specific regions without repeating the surrounding wrapper.
Prerequisites
- A Node.js project with the
pugpackage installed (npm i pug). - A
.pugtemplate file where the mixin will live. - Comfort with Pug's indentation-based syntax: nested elements are expressed by leading whitespace, not closing tags.
- Understanding of the difference between plain text lines, tag lines, and JavaScript expressions prefixed with
=or-.
Focused procedure
1. Define the mixin with block placeholders
A mixin in Pug is a reusable, parameterized chunk of markup. A block is a named placeholder inside that mixin. Place the mixin at the top level of your template or in a shared file that you include:
mixin card(title, classes='')
.card(class=classes)
h2.card-title= title
block card-body
p.card-text No body provided.
Every line indented under mixin card(...) belongs to the mixin body. The block card-body declaration marks a replaceable region. The paragraph indented beneath it is the default content that renders when a caller does not override the block.
2. Call the mixin and override blocks
Invoke the mixin with a leading plus sign: +card(...). To replace a block, append a matching block declaration at the same indentation level as the mixin arguments:
+card('User Profile', 'profile-card')
block card-body
img(src='avatar.png' alt='Avatar' class='avatar')
p Name: John Doe
p Role: Developer
The content under the call-site block card-body replaces the default paragraph inside the mixin. You can override multiple named blocks in a single invocation as long as each name matches a declaration inside the mixin.
3. Forward HTML attributes without repeating them
When a component needs to accept arbitrary HTML attributes, use the special attributes keyword. It merges an object of attributes into the preceding tag:
mixin button(label, extraAttrs={})
button(class='btn' &attributes(extraAttrs))= label
Call site:
+button('Save', { type: 'submit', 'data-action': 'save' })
This keeps the mixin signature small while still allowing callers to add type, data-*, ARIA attributes, or event hooks.
Expected checks
- Run
npx pug template.pugfrom your project directory. No special filesystem permissions are required. - Inspect the generated HTML. The wrapper (
.card) should appear exactly once per+cardinvocation, and the injected block content should replace the default paragraph. - Comment out the call-site
block card-bodyoverride and re-render. The fallback text "No body provided." should appear, confirming the default block is wired correctly. - Check that no stray indentation was introduced. Pug is indentation-sensitive; mixing tabs and spaces in the mixin body can cause parse errors or silent misnesting.
Recovery options for common authoring errors
- Mixin not defined: Verify the declaration starts at column zero with the
mixinkeyword. If the file uses tabs elsewhere, convert the mixin body to spaces. - Block content missing: Confirm the block name at the call site matches the declaration exactly, including case.
block card-bodyandblock cardBodyare different placeholders. - Duplicated or missing wrappers: This usually means indentation is inconsistent. Re-indent the mixin body so every nested line uses the same number of spaces per level.
- Stopgap fallback: If a mixin is blocking progress, extract the shared markup into a separate
.pugfile and useincludeuntil the mixin is corrected. Remember thatincludecopies raw markup and does not isolate scope.
Engineering decision: mixins with blocks vs. include
Both mixins and include reduce duplication, but they solve different problems. Use include when you need to paste a static fragment into multiple templates. Use a mixin with blocks when the component needs varying inner content.
| Concern | Mixin with blocks | Include |
|---|---|---|
| Parameterization | Accepts arguments and named blocks | Static text only |
| Scope isolation | Body runs in mixin scope; local variables do not leak | Copied markup shares the including template's scope |
| Call-site clarity | Concise +component(...) with block overrides | Requires repeating the full fragment or relying on conditionals |
Because mixins are functions, they prevent accidental variable leakage from the included file's scope and keep the call site readable.
Limitations and practical verification
Blocks inside mixins are scoped to the invocation. They do not participate in template inheritance chains driven by extends and block. If you rely on layout inheritance, keep mixin blocks separate from inheritance blocks to avoid confusion.
Deeply nested mixin calls can produce stack traces that point to compiled JavaScript line numbers rather than original template lines. While debugging, keep nesting shallow and render intermediate templates to isolate the failing mixin.
To verify the final result, render the template and compare the output against an expected HTML snippet. For example, given the mixin and call site above, the rendered HTML would contain a single <div class="card profile-card"> wrapper with the avatar image and profile paragraphs inside it. If the override is omitted, the wrapper would instead contain the default paragraph.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.