Build Reusable UI Components with Pug Mixins
Use Pug mixins to render reusable tables and components from one parameterized template instead of copying markup across pages.
04 Aug 2026, 02:07 UTC

The problem: copying the same table three times
You are maintaining an admin dashboard in Pug. Three pages each render a table—users, orders, products—so you copy the same <table>, <thead>, and <tbody> structure into every file. When the design team asks for a data-table class on every table, you edit three files and still miss one. That repetition also raises regression risk: a one-line design change becomes a multi-file search-and-replace job.
A Pug mixin is a named, reusable block of template markup that accepts parameters. You define it once, call it with different data, and Pug renders the HTML when the template is compiled. Mixins can also accept a block of content passed by the caller, but this example uses parameters only. This post shows a table mixin, a render check, and the trade-offs to keep in mind.
Define the mixin once
Pug is a Node.js template engine that compiles .pug files into HTML. A mixin is declared with the mixin keyword and invoked with +name(...). Parameters are local to the mixin, so the same block can render different data without leaking variables between calls. Two calls can even reuse the same parameter names without colliding.
mixins.pug
mixin dataTable(columns, rows)
table.data-table
thead
tr
each col in columns
th= col
tbody
each row in rows
tr
each col in columns
td= row[col]
Call it from a page
The page below extends a base layout and calls the mixin twice with different column lists and data arrays.
layout.pug
doctype html
html
head
title My App
body
block content
index.pug
extends layout.pug
block content
h1 Users
+dataTable(['id', 'name', 'email'], users)
h1 Products
+dataTable(['sku', 'title', 'price'], products)
Render and verify the output
Run these commands in a terminal inside your project directory. No special permissions are required beyond normal file creation.
mkdir pug-mixin-demo
cd pug-mixin-demo
npm init -y
npm install pug@3
pug@3 is a version assumption; the mixin syntax has been stable since Pug 2.x. Create render.js in the same directory:
const pug = require('pug');
const html = pug.renderFile('index.pug', {
users: [
{ id: 1, name: 'Alice', email: '[contact removed]' }
],
products: [
{ sku: 'A-100', title: 'Widget', price: '9.00' }
]
});
console.log(html);
Run node render.js. The expected check is that the printed HTML contains <table class='data-table'>, <th>email</th>, and <td>[contact removed]</td> for the first table, followed by a second table with product headers.
Risk: if rows is undefined, Pug throws a ReferenceError. If a column name does not exist on a row object, Pug renders an empty cell instead of failing. That silent behavior means a typo in a column name can produce a blank column without an obvious error.
Trade-offs and when not to use a mixin
- Deeply nested mixins make stack traces harder to read, so keep component nesting shallow.
- Mixins that read global variables create hidden dependencies and reduce template portability. Pass data through parameters instead.
- For static markup that never changes, a plain
includeis simpler than a parameterized mixin. - Pug does not type-check mixin parameters, so document the expected shape of
columnsandrowsfor future maintainers. - Render a single mixin in isolation before composing it into a page; otherwise a rendering error in a nested mixin can be hard to trace to the right file.
A practical way to check the result is to grep the rendered HTML for the expected header count, or write a small script that asserts the number of <th> elements matches the length of the columns array.
Actionable closing
Pick one repeated component in your current project—a table, form control, or card—and extract it into a mixin with two parameters: the data and the configuration. Add a comment above the mixin documenting the expected parameter shape. Render it in one place first, verify the HTML, then reuse it across pages. The goal is less duplicated markup, not more abstraction.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.