Browser printing has always been the awkward corner of web development. window.print() gives you a print dialog and a prayer. For consumer sites that's fine. For warehouse systems, production floors, or anything that needs to talk directly to label printers on a predictable schedule, it falls apart immediately.
QZ Tray solves this by running a small desktop application that opens a WebSocket server on the user's machine. Your web app connects to it, sends structured print jobs, and QZ Tray handles the actual printer communication. No browser dialog, no PDF detour, no guessing about paper size.
react-qztray is a hooks library I built to make this integration less painful in React.
QZ Tray is a desktop application the end user installs once. It runs in the background and listens at wss://localhost:8181 by default.
In production, every request must be cryptographically signed. QZ Tray verifies the signature against a certificate you provide, so you need two things: A certificate/key pair generated by QZ Tray's signing tool A backend endpoint that signs requests on behalf of your frontend
The signing request is a simple HTTP call with a string in and a string out, so the backend side is minimal.
QZ Tray's own JavaScript library is bundled internally, so there's no separate qz-tray.js install.
QZ Tray passes a string to your signaturePromise function. You send that string to your backend, your backend signs it with the private key and returns the signature, then QZ Tray verifies it against the public certificate.
The private key stays on the server. The public certificate goes in your React app and is safe to expose.
QzTrayProvider sets up the context for all child hooks. Wrap it around whichever part of your app needs printing, whether that's the root or a specific route.
The signaturePromise shape looks unusual at first. It's a curried factory: receives toSign, returns a function that receives resolve. This is how QZ Tray's internal API works, so the library matches it directly.
If you need to fetch the certificate at runtime rather than hardcoding it, certificate also accepts an async function:
For dedicated print workstations where QZ Tray is always running, autoConnect on the provider saves the manual connect step:
connect() will throw if QZ Tray isn't running. Wrap it in a try/catch when calling it manually, or just let the error state surface it to the user.
useQzPrint submits the actual jobs. It connects automatically if the WebSocket isn't open yet, so calling connect() first is optional.
If you're composing content in HTML, the pixel type with html format handles it:
For Zebra printers (ZPL) or receipt printers (ESC/POS), use type: 'raw'. QZ Tray passes the commands straight to the printer without any rendering step:
The printer name must match exactly what the OS reports, including model number and driver suffix. The safest way to get it right is to open any print dialog on the target machine and copy the name verbatim.
