Ever had to print 200 warehouse bin labels and realized there's no clean way to do it from a web app?
That's what pushed me to build qrlayout-core and qrlayout-ui — two open source npm packages for designing QR label templates and bulk-printing them from React. This post walks through the full workflow with real code.
The Problem
Printing QR labels from a web app usually means one of these:
- Generating them one at a time manually
- Hardcoding the label layout in your codebase
- Exporting a CSV and using a desktop app
None of these scale. And none of them give non-technical users control over the label design.
The Solution: Two Packages, One Pipeline
qrlayout-ui — a drag-and-drop label designer your users can embed directly in your app
qrlayout-core — the headless rendering engine that turns saved templates + data into PNG, PDF, or ZPL
npm install qrlayout-core qrlayout-ui
The workflow looks like this:
Labels Tab → Design a template, save it with a title + entity type
Master Table → Select that saved layout from a dropdown
Table Rows → Check the records you want
Export → PNG · PDF · ZPL (thermal printer)
The key idea: template and data stay separate. One layout generates labels for hundreds of records. And because each template is tagged to an entity (employee, machine, storage), the master table only shows layouts relevant to it.
Step 1: Define Entity Schemas
You tell the designer what fields each entity has. This powers the live preview — every {{field}} placeholder dropped on the canvas renders with real sample data instantly.
const SAMPLE_SCHEMAS = {
storage: {
label: "Storage Master",
fields: [
{ name: "binCode", label: "BIN Code" },
{ name: "storageType", label: "Storage Type" },
{ name: "aisle", label: "Aisle" },
{ name: "rack", label: "Rack Number" },
],
sampleData: {
binCode: "BIN-A1-R4", storageType: "Pallet Rack",
aisle: "Aisle 01", rack: "R-44"
}
},
// employee and machine follow the same shape
};
Step 2: Mount the Designer in React
qrlayout-ui is framework-agnostic so you mount it with a ref + useEffect. Two things matter most — passing onSave to capture the layout JSON, and calling destroy() on unmount to prevent memory leaks.
useEffect(() => {
if (!containerRef.current) return;
designerRef.current = new QRLayoutDesigner({
element: containerRef.current,
entitySchemas: SAMPLE_SCHEMAS,
initialLayout: editingLayout || { ...DEFAULT_NEW_LAYOUT, id: crypto.randomUUID() },
onSave: (layout) => {
storage.addLabel(layout); // persist the layout JSON
setLabels(storage.getLabels());
setSubView('list');
}
});
return () => {
designerRef.current?.destroy(); // always clean up
designerRef.current = null;
};
}, [subView, editingLayout]);
When the user clicks Save inside the designer, onSave fires with the complete StickerLayout JSON — name, target entity, dimensions, and all elements. That single JSON object is what flows through the entire rest of the system.
Step 3: The Master Table — Select Rows, Pick Layout, Export
Each master table (Employees, Machines, Storage BINs) filters layouts by targetEntity so users only see relevant templates in the dropdown.
// Only load layouts relevant to this entity
const binLabels = storage.getLabels().filter(l => l.targetEntity === 'storage');
const getSelectedBins = () => bins.filter(b => selectedBinIds.includes(b.id));
const getActiveLayout = () => labels.find(l => l.id === selectedLayoutId);
const handleExportPDF = async () => {
const layout = getActiveLayout();
const selected = getSelectedBins();
if (!layout) return;
await exportToBatchPDF({
layout,
items: selected,
printer: printer.current,
baseFilename: 'batch-bin-labels'
});
};
When rows are selected, the export toolbar appears with three options. Until then the page shows a step-by-step guide so users know what to do.
// PNG — one file per item
printer.renderToDataURL(layout, item, { format: 'png' })
// PDF — all items in one document
import { exportToPDF } from 'qrlayout-core/pdf';
const pdf = await exportToPDF(layout, items);
pdf.save('labels.pdf');
// ZPL — for Zebra and other thermal printers
const zplCommands = printer.exportToZPL(layout, items);
// In production: send over TCP port 9100 directly to the printer
exportToPDF is a separate optional import to keep the core bundle light. ZPL output is an array of strings — one per label — which you can download as a .txt file or pipe straight to a Zebra printer over TCP.
Try It — Pre-Loaded Demo Data
The live demo seeds everything on first load:
- 3 layouts — Professional ID Badge, Equipment Asset Tag, Storage Location Label
- 3 employees — Arjun Mehta, Priya Sharma, Kiran Patel
- 3 machines — CNC Router X1, Industrial 3D Printer, Hydraulic Press
- 3 bins — BIN-A1-R1 (Pallet Rack), BIN-A1-R2 (Shelf), BIN-B2-R1 (Cold Storage)
Go to Storage → check all bins → click PDF. Done in 30 seconds.
This is a Frontend Demo — Here's the Production Path
Everything in the demo runs in the browser using localStorage. In a real project you'd swap each layer with a backend equivalent:
| Demo | Production |
localStorage (layouts) | DB table with a layout_json column |
localStorage (bins/machines) | Your existing ERP or database |
Client-side filter by targetEntity | WHERE target_entity = 'storage' |
| Browser PDF download | Node.js server-side render → stream to client |
.txt ZPL download | TCP socket → Zebra printer (port 9100) |
qrlayout-core has zero browser dependencies — exportToPDF and printer.exportToZPL work identically in Node.js. The full production flow:
React Frontend
└── qrlayout-ui → user designs label → onSave fires layout JSON
└── POST /api/layouts → save to DB
Node.js Backend
└── GET /api/layouts?entity=storage → filtered layouts to frontend
└── POST /api/labels/print
├── fetch layout from DB
├── fetch selected records from ERP / DB
├── exportToPDF(layout, items) → stream PDF to client
└── printer.exportToZPL(layout, items) → TCP → Zebra printer
Links
Happy to answer questions about the implementation in the comments.