Every few weeks someone searches for "javascript print without dialog", finds a Stack Overflow answer from 2013, and spends two days discovering that none of it works anymore.
The request is completely reasonable: a cashier taps Complete Sale, a receipt comes out. A warehouse app renders a label, the label prints. Nobody should have to click through a print dialog and pick a printer forty times an hour.
The problem is that the browser is the wrong place to ask for it, and understanding why saves you from a long detour through workarounds that all fail the same way.
Why window.print() can't do what you want
window.print() opens the browser's print dialog. That dialog is not a UI nicety that a flag can turn off — it is a security boundary. If a web page could silently push bytes to hardware, any ad iframe could burn through your paper tray, and any page could learn what printers you own for fingerprinting. So the browser insists on a human deciding.
That boundary has consequences beyond the click:
- You cannot choose the printer. The page can't enumerate printers or select one. If the receipt printer isn't the OS default, the user picks it — every time.
- You cannot control the output faithfully. Margins, headers, scaling, and page breaks come from the dialog's settings and the browser's print CSS implementation. The same page prints differently in Chrome and Safari.
- You get no result.
window.print()returnsundefined. There is no "it printed", no "the printer was offline", no job id.afterprintfires when the dialog closes, whether or not anything came out. - It only exists while the tab does. Close the tab mid-job and the state is gone.
For printing a user's own document — a report they're reading, a page they want on paper — the dialog is correct, and window.print() with a decent @media print stylesheet is the right answer. Do that, and stop reading.
Everything below is for the other case: a machine printing at a fixed station as part of a workflow.
The workarounds, and why they keep failing
Chrome's --kiosk-printing flag. Launch Chrome with --kiosk-printing and window.print() prints to the default printer with no dialog. It genuinely works — on that one machine, launched that one way. Then someone opens Chrome from the taskbar instead of your shortcut and the dialogs come back. Or Windows switches the default printer to "Microsoft Print to PDF" after an update, and receipts start silently becoming files. You still get no status back, and you now have a per-machine setup step for every new terminal.
The dead plugin era. NPAPI plugins, Java applets, ActiveX controls, document.execCommand('print') in IE — all removed. Any answer relying on them is a museum piece.
Auto-print PDFs. Embedding /OpenAction << /S /JavaScript /JS (this.print\(true\);) >> in a PDF made Acrobat print on open. Chrome's built-in PDF viewer does not execute PDF JavaScript, and behavior varies across every other viewer. You cannot depend on it.
Printing an iframe. iframe.contentWindow.print() is the same dialog, scoped to an iframe. It solves layout isolation, not silence.
A local helper the page talks to. A small program on the machine listens on localhost, the page posts jobs to it, the helper drives the printer. This is the shape that actually works — QZ Tray, JSPM, and the local agent described below are all versions of it. The honest caveats: something must be installed and kept running on every machine, an HTTPS page talking to http://localhost runs into origin and private-network rules, and you inherit the job of signing, updating, and supporting a desktop binary.
Raw sockets to the printer. Browsers have no TCP. fetch() to port 9100 is not a thing, and no amount of clever will make it one.
What works: move the decision to the server
Notice the pattern in every failure above: the browser doesn't know which printer, can't be trusted with the hardware, doesn't outlive the tab, and can't tell you what happened. All four go away when the browser only expresses intent and the backend does the printing.
Browser: "order 1042 is complete"
↓ (normal API call to your own backend)
Your server: decides document + printer, calls the print API
↓
Cloud print service → agent at the store → printer
↓
Webhook back to your server: printed / failed, with a reasonThe front end sends a single ordinary request to your own API — no plugins, no flags, no localhost:
async function completeSale(orderId) {
await fetch(`/api/orders/${orderId}/complete`, { method: 'POST' });
// that's it — no print dialog, no printer picker
}Your backend generates the document and sends it to the printer at that location. With PrintBase that's one HTTPS call:
// POST /api/orders/:id/complete
import { renderReceipt } from './receipt.js';
export async function completeSale(order) {
const escpos = renderReceipt(order); // ESC/POS bytes for a thermal printer
const store = await db.getStore(order.storeId); // knows its printer_code
const res = await fetch('https://api.printbase.cloud/v1/print-jobs', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PRINTBASE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
printer_code: store.receiptPrinterCode,
content_type: 'raw', // pass ESC/POS straight through
content: escpos.toString('base64'),
copies: 1,
}),
});
const job = await res.json();
await db.recordPrintJob(order.id, job.id); // job_abc, status: queued
return job;
}For a PDF — an invoice, a picking list, a ticket — it's the same call with content_type: 'pdf' and either base64 content or a content_url the agent downloads.
What you gain is exactly what the browser could never give you:
- The right printer, always.
printer_codecomes from your database, not from whatever a user last selected. Receipts go to the receipt printer; labels go to the label printer. - Byte-exact output.
rawcontent passes through untouched, so an ESC/POS receipt or a ZPL label prints identically on every machine — no print CSS, no driver reinterpretation. - A job id and a real result. Subscribe to webhooks and you learn that the job
completed, or that itfailedwithPRINTER_OFFLINEonwh-pc-02. That's the difference between a receipt system and a receipt system you can operate. - Nothing to install per browser. One small agent per location, not a plugin per machine, per browser, per profile.
When the printer is on the user's own machine
Not every case is a fixed station. Sometimes the document belongs to whoever is sitting there, and it must come out of their printer — a user printing their own shipping label at home.
If a dialog is acceptable, window.print() remains the right tool. If it isn't, you're back to installing something locally, and the honest framing is that this is a desktop-software problem wearing a web-app costume. The PrintBase agent supports this mode: enable local web printing and it listens on http://127.0.0.1:17891, accepting the same POST /v1/print-jobs shape, printing immediately, then recording the job to the cloud asynchronously. It's genuinely useful for localhost development and inside the PrintBase dashboard — but it only accepts requests from localhost and printbase.cloud origins, so your own app at app.example.com can't call it directly. For everything else, route through the cloud API and let the agent stay a background detail.
Choosing, in one table
| Situation | Use | Dialog? |
|---|---|---|
| User prints a page they're reading | window.print() + print CSS | Yes, and that's fine |
| Fixed station: POS, warehouse, kitchen | Backend → cloud print API → agent | No |
| Backend event triggers a print (order paid, label bought) | Cloud print API, no browser involved | No |
| Must hit the end user's own printer, silently | Install a local helper — accept the support burden | No |
| Anything relying on plugins, kiosk flags, or PDF auto-print | Don't | It'll break |
The short version
JavaScript can't print silently, and no flag or trick reliably changes that — the dialog is a security boundary, not a missing feature. What the browser is missing isn't permission, it's information: which printer, which format, and what happened afterwards. Your server has all three.
Let the page say what happened. Let the backend decide what prints where. Then "print without a dialog" stops being a browser problem and becomes an ordinary API call.
PrintBase is that API call: your backend POSTs a job, a lightweight agent next to the printer receives it over an outbound connection, and webhooks tell you what actually came out. The free plan is 100 jobs a month with no card — enough to replace a kiosk-mode hack in an afternoon. Start with the docs, or the receipt printing API overview if a POS or kiosk is what you're building.
