Same guide as the project README — how printing works, which language to pick (TSPL / ZPL / EPL), and what to do when the printer does not move.

Barcode Label Studio

Design barcode/shipping labels in the browser and print them to virtually any label or thermal printer - from a $20 USB thermal printer to a networked Zebra industrial printer - all with local-only storage (no server, no accounts).

Features

  • Label designer - drag-and-drop canvas (text, barcodes/QR/DataMatrix/PDF417/Aztec,

images, rectangles, lines) with property panel, undo/redo, zoom, grid and snapping.

  • Template library - CRUD dashboard with JSON import/export, all stored locally in

your browser (IndexedDB via Dexie) - nothing leaves your machine.

  • Two printing paths, from day one:

- Universal: renders an exact-size PDF (via pdf-lib) and hands it to your OS print dialog - works with any printer you already have a driver for. - Raw/driverless: compiles the label straight to ZPL, TSPL, or EPL and sends it via QZ Tray to USB/serial/network thermal printers, with no OS driver needed.

  • Printer profiles - connection type, PDL language, DPI, darkness, speed, and

media (gap/black-mark) calibration, with a one-click test print.

  • Batch/variable-data printing - import a CSV, map columns to label fields, preview

the first rows, and print the whole batch through either path.

Working with printers

There are two delivery paths. Pick the one that matches the hardware, then save it as a printer profile under Printers.

PathWhat it sendsWhen to use it
System print dialogA PDF, through the OS driverInkjet/laser, Brother QL, Dymo, or any thermal printer that already prints well from Preview / Word
Raw via QZ TrayNative ZPL, TSPL, or EPL bytesTSC, Zebra, Godex, iDPRT, Xprinter, Rongta, and most $20 USB 4×6 thermal printers

The system-dialog path needs no extra software. Raw thermal printing needs QZ Tray running on the computer the printer is plugged into - the browser cannot talk to USB printers by itself.

Command language (this is the setting that silently fails if it is wrong)

Thermal printers do not accept "a label". They accept a command language, and they ignore any other language with no error and no paper movement. The profile's Printer language must match the printer's current command mode, not merely the brand on the box.

Language in this appTypical printersHow to confirm
TSPL / TSPL2TSC TE / TTP / DA series, Godex, iDPRT / Label TD-401, Xprinter, Rongta, Raiser, most generic USB shipping-label printersPrinter menu / self-test page says command mode TSPL. Test print that is accidentally ZPL does nothing.
ZPL / ZPL IIZebra ZT / ZD / GK / GX, or a TSC in TSPL-EZ / ZPL emulationSelf-test or Zebra driver. Test print that is accidentally TSPL does nothing.
EPL / EPL2Legacy Eltron and some older Zebra LP/TLP unitsRare on new hardware.

TSC TE244 (and many clones) ship in TSPL. Some TSC units can switch to ZPL emulation (TSPL-EZ) from the panel - if you switch the printer, switch the profile too. Selecting the TSC TE244 OS queue while leaving language on ZPL is the most common "Test print succeeded, printer never moved" failure.

When you pick a queue whose name looks like TSC / TE244 / TD-401 / Godex / Xprinter, the printer form auto-sets language to TSPL. Always double-check it.

Set up a raw thermal printer

  1. Plug the printer in over USB (or put it on the LAN). Confirm it has power and

media loaded.

  1. Install and launch QZ Tray. Leave it running. The

first connection from this app shows QZ Tray's "Allow this website?" prompt - it can open behind other windows; click Allow.

  1. In this app: Printers → New printer.
    • Connection: Raw via QZ Tray
    • Printer language: TSPL (TSC/generic), ZPL (Zebra), or EPL
    • QZ Tray printer: click refresh and pick the queue for the device that is

actually plugged in (see USB queue must match the cable below) - DPI: 203 for almost all 4×6 desktop thermals (TE244, TD-401, GK420) - Save, then Test print

  1. If the test label comes out, use that same profile on Print. If it does

not, work through the troubleshooting below before changing darkness/speed.

USB queue must match the cable

macOS (and Linux) print to a named queue, and that queue is permanently bound to one USB identity. Plugging in a different thermal printer, or unplugging the one the queue was created for, makes the queue go offline. QZ Tray still reports "Printing complete"; CUPS just holds the job forever.

Check:

lpinfo -v | grep usb    # what is physically plugged in right now
lpstat -v               # which USB URI each queue will send to
lpstat -p               # offline / idle

The URI on the queue you select in this app (lpstat -v) must match a line from lpinfo -v. Example of a mismatch that produces no printer action:

lpinfo -v   →  usb://Label/TD-401?serial=164HS010781     # plugged in
lpstat -v   →  usb://TSC/TE244?serial=000001             # queue you printed to
lpstat -p   →  TSC_TE244 ... The printer is offline.

Fix: either plug the original TSC TE244 back in, or create a queue for the device that is actually connected and select that queue in the app.

macOS: recent versions no longer allow a CUPS "Raw Queue" (lpadmin -m raw fails with "Raw queues are no longer supported"). That is fine - this app already asks QZ Tray to print with forceRaw: true (lp -o raw), which bypasses the vendor PPD. Any queue whose device URI matches is enough:

lpinfo -v | grep usb          # e.g. usb://Label/TD-401?serial=...
lpadmin -p Label_TD401 -E -v "usb://Label/TD-401?serial=..." \
  -m drv:///sample.drv/generic.ppd

Then refresh the QZ printer list and select Label_TD401. Network printers use socket://<printer-ip>:9100 as the -v URI instead.

Clear stuck jobs on an offline queue so they do not dump all at once when the device returns:

cancel -a TSC_TE244

Windows: forceRaw does not bypass the OS driver. Send raw TSPL/ZPL to a Generic / Text Only queue, not the TSC/Zebra manufacturer driver:

  1. Note the existing printer's port (Printer properties → Ports, e.g. USB001).
  2. **Settings → Printers & scanners → Add device → The printer that I want isn't

listed → Add a local printer with manual settings**.

  1. Use an existing port → the same port from step 1.
  2. Manufacturer GenericGeneric / Text Only. Name it e.g. Label_RAW.
  3. In this app, refresh QZ printers and select Label_RAW.

A manufacturer/GDI queue either prints the command text as a document, or silently drops the job. Both mean you are not on Generic / Text Only.

Media, DPI, and darkness

Set these on the printer profile so a Test print is sized for the stock in the printer (the template's media can still describe a different label):

  • DPI - 203 for TSC TE244 / TD-401 class; 300 only if the printer is actually

300 dpi.

  • Darkness - start around 15/30. Too low looks grey; too high bleeds barcodes.
  • Speed - 4 ips is a safe default; 0 leaves the printer's own setting.
  • Media sensing - gap (most die-cut labels), black mark, or continuous. Wrong

sensing usually *does* move paper (feed/calibrate loop), unlike a language or offline-queue miss.

If the printer outputs the literal ZPL/TSPL text instead of the label

The OS queue is rendering the job as a text document instead of passing bytes through. Use a passthrough queue as in the macOS / Windows steps above, not the printer's normal document driver.

QZ Tray trust prompt

By default QZ Tray will show a one-time trust prompt per session, since this app isn't signed with a paid QZ certificate. To suppress that prompt, generate/obtain a QZ signing certificate and set these environment variables before building/running:

QZ_CERTIFICATE="-----BEGIN CERTIFICATE-----..."
QZ_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..."

The private key is only ever used server-side (in /api/qz/sign) to sign requests; it is never sent to the browser.

Chrome blocks QZ Tray (ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS)

This is a Chrome/Edge security feature called Local Network Access (LNA), not a bug in this app or QZ Tray. Chrome blocks any page loaded from a public address (e.g. this app's deployed URL) from opening a connection to localhost/a private-network address (e.g. QZ Tray's WebSocket on the visitor's own machine) - and per Chrome's docs, *the permission prompt that would allow it can only be requested from a secure context (HTTPS)*. Served over plain http://, the browser silently blocks the connection with no prompt at all.

Fix: serve the app over HTTPS. Once you access the app via https://..., the first QZ Tray connection attempt should trigger Chrome's own "Allow this site to access devices on your local network?" prompt (in addition to, and separate from, QZ Tray's own certificate-trust dialog) - accept both once per browser/machine.

Known limitations (good next steps)

  • QR/DataMatrix/PDF417/Aztec sizing in raw ZPL/TSPL output uses heuristic magnification

rather than exact module-count calculation.

  • TSPL/EPL Data Matrix, PDF417 and Aztec fall back to a rasterized bitmap rather than

the printer's native 2D symbol command, for cross-firmware reliability.

  • Brother QL / Dymo LabelWriter use proprietary raster protocols and are best driven via

their own OS drivers through the universal PDF path rather than a hand-rolled raw mode.

  • WebUSB/Web Serial, as an installer-free alternative to QZ Tray, is not implemented.