Print to thermal (ESC/POS) and A4 (PDF) printers over TCP, Bluetooth, USB, system spooler, or WebUSB / Web Serial — works in Node.js, Bun, Electron, Expo, and the browser.
npm install universal-thermal-printer| Runtime | Version |
|---|---|
| Node.js | >= 18 (for net module) |
| Bun | >= 1.0 |
| Electron | >= 28 (uses Node.js net module) |
| Browser | Chrome / Edge >= 89 (WebUSB + Web Serial) |
Only the transport packages you actually use are required — everything else is optional:
| Dependency | Runtime | Required for | Install |
|---|---|---|---|
bluetooth-serial-port |
Node.js / Bun | Classic Bluetooth (SPP) | npm install bluetooth-serial-port |
react-native-ble-plx |
Expo | BLE Bluetooth | npx expo install react-native-ble-plx |
react-native-tcp-socket |
Expo | TCP/Network | npx expo install react-native-tcp-socket |
serialport |
Node.js | USB serial | npm install serialport |
pdf-lib |
Node.js / Bun / Expo | A4/PDF printing | npm install pdf-lib |
| — (built-in) | Expo (Android) | USB native | Included in universal-thermal-printer |
TCP printing on Node.js/Bun uses the built-in net module — zero additional packages needed.
On Expo, TCP requires react-native-tcp-socket and a development build (not Expo Go).
USB printing on Expo (Android) uses a built-in native module — just add the config plugin.
Calling a transport function without its optional package installed throws a clear error telling you exactly what to install.
# Node.js: TCP-only (zero extra deps)
npm install universal-thermal-printer
# Node.js: all transports
npm install universal-thermal-printer bluetooth-serial-port serialport
# Expo: all transports (requires dev build)
npx expo install universal-thermal-printer react-native-ble-plx react-native-tcp-socketimport { print, listBluetoothPrinters, listUsbPrinters, listSpoolerPrinters, listNetworkPrinters } from "universal-thermal-printer";
import type { PrintSection } from "universal-thermal-printer";import { print, requestUsbDevice, listUsbPrinters, requestSerialDevice } from "universal-thermal-printer/web";WebUSB and Web Serial transports are available via the "/web" sub-path export.
Print sections to a thermal printer.
| Arg | Type | Description |
|---|---|---|
type |
"network" | "bluetooth" | "usb" | "spooler" |
Transport type |
address |
string |
IP address, Bluetooth MAC, USB path, or printer name. On Expo Android use "vendorId:productId" for USB. In browser use deviceId from requestUsbDevice(). |
sections |
PrintSection[] |
Array of print commands |
options.port |
number (default 9100) |
TCP port (network only) |
options.baudRate |
number (default 9600) |
Baud rate (Node.js USB via serialport only) |
options.format |
"thermal" | "a4" (default "thermal") |
Output format — ESC/POS or PDF |
options.pageSize |
"A4" | "Letter" (default "A4") |
Page size (A4 format only) |
options.margins |
number (default 40) |
Page margins in points (A4 format only) |
options.title |
string |
Optional centered title at top (A4 format only) |
Returns: Promise<string> — success message.
Throws when the transport package is not installed or connection fails.
List paired/available Bluetooth printers.
async function listBluetoothPrinters(): Promise<{ name: string; address: string }[]>| Runtime | Backend | Behavior |
|---|---|---|
| Node.js / Bun | bluetooth-serial-port |
Lists paired SPP devices |
| Expo | react-native-ble-plx |
Scans for BLE peripherals (5s timeout) |
Throws if the optional dependency is not installed.
List connected USB printer devices.
async function listUsbPrinters(): Promise<{ name: string; deviceId: string }[]>| Runtime | Backend | Behavior |
|---|---|---|
| Node.js / Bun | serialport |
Lists USB serial devices |
| Expo | Built-in native module (Android) | |
| Expo (iOS) | — | Always throws |
| Browser | WebUSB | Lists previously paired devices (no chooser) |
Discover network printers via mDNS/Bonjour + SNMP + port scan.
async function listNetworkPrinters(options?): Promise<{ name: string; address: string; port: number }[]>| Runtime | Backend |
|---|---|
| Node.js / Bun | Multicast DNS + SNMP queries + TCP scan |
List system-installed printers (spooler).
async function listSpoolerPrinters(): Promise<{ name: string; deviceId: string }[]>| Runtime | Backend |
|---|---|
| Node.js / Bun | Windows: PowerShell WMI; macOS: lpstat; Linux: lpstat |
import { print } from "universal-thermal-printer";
await print("network", "192.168.1.87", [
{ type: "Init" },
{ type: "Size", value: { width: 2, height: 2 } },
{ type: "Align", value: "center" },
{ type: "Bold", value: true },
{ type: "Text", value: "HELLO WORLD" },
{ type: "Bold", value: false },
{ type: "Feed", value: 3 },
{ type: "Cut" },
]);import { print, listBluetoothPrinters } from "universal-thermal-printer";
const printers = await listBluetoothPrinters();
await print("bluetooth", printers[0].address, [
{ type: "Init" },
{ type: "Text", value: "Hello from Bluetooth!" },
{ type: "Cut" },
]);import { print, listUsbPrinters } from "universal-thermal-printer";
const devices = await listUsbPrinters();
await print("usb", devices[0].deviceId, [
{ type: "Init" },
{ type: "Text", value: "USB print works too" },
{ type: "Cut" },
], { baudRate: 9600 });import { print, listUsbPrinters } from "universal-thermal-printer";
const devices = await listUsbPrinters();
await print("usb", devices[0].deviceId, [
{ type: "Init" },
{ type: "Text", value: "Hello from Android USB!" },
{ type: "Cut" },
]);import { print } from "universal-thermal-printer";
await print("spooler", "My Printer Name", [
{ type: "Init" },
{ type: "Align", value: "center" },
{ type: "Bold", value: true },
{ type: "Size", value: { width: 3, height: 3 } },
{ type: "Text", value: "INVOICE" },
{ type: "Bold", value: false },
{ type: "Line", value: "-" },
{ type: "Text", value: "Item 1 ................ $10.00" },
{ type: "Text", value: "Item 2 ................ $20.00" },
{ type: "Text", value: "Total: $30.00" },
{ type: "Feed", value: 3 },
], {
format: "a4",
title: "My Store",
pageSize: "A4",
});import { print, requestUsbDevice } from "universal-thermal-printer/web";
// Shows browser chooser dialog — user selects printer
const device = await requestUsbDevice();
await print("usb", device.deviceId, [
{ type: "Init" },
{ type: "Align", value: "center" },
{ type: "Size", value: { width: 2, height: 2 } },
{ type: "Text", value: "Hello from the browser!" },
{ type: "Feed", value: 3 },
{ type: "Cut" },
]);Requires Chrome/Edge 89+ on HTTPS or localhost. See docs/browser.md for Web USB troubleshooting (Zadig driver fix).
import { print } from "universal-thermal-printer";
// macOS/Linux: CUPS lp, Windows: PowerShell .NET
await print("spooler", "POS80 Printer", [
{ type: "Init" },
{ type: "Text", value: "Print via system spooler" },
{ type: "Cut" },
]);Expo Go vs dev build:
react-native-tcp-socketandreact-native-ble-plxrequire a development build (npx expo run:ios/npx expo run:android). They do not work in Expo Go.
npx create-expo-app@latest my-print-app
cd my-print-app
npx expo install universal-thermal-printer react-native-tcp-socket react-native-ble-plxConfigure app.json:
{
"expo": {
"plugins": [
"universal-thermal-printer/plugin/withUsbPrinter"
],
"ios": {
"infoPlist": {
"NSBluetoothAlwaysUsageDescription": "Connect to thermal printers",
"NSBluetoothPeripheralUsageDescription": "Connect to thermal printers"
}
}
}
}The config plugin auto-detects your Android SDK and creates android/local.properties if missing — no manual setup needed.
npx expo run:android # Android dev build
npx expo run:ios # iOS dev buildimport { useState } from "react";
import { Button, View, TextInput } from "react-native";
import { print, listBluetoothPrinters } from "universal-thermal-printer";
export default function PrintScreen() {
const [ip, setIp] = useState("192.168.1.87");
const [printers, setPrinters] = useState([]);
async function scanBt() {
const list = await listBluetoothPrinters();
setPrinters(list);
}
return (
<View>
<TextInput value={ip} onChangeText={setIp} placeholder="Printer IP" />
<Button title="Print via Network" onPress={() =>
print("network", ip, [
{ type: "Init" },
{ type: "Bold", value: true },
{ type: "Align", value: "center" },
{ type: "Text", value: "Hello from Expo!" },
{ type: "Feed", value: 3 },
{ type: "Cut" },
])
} />
<Button title="Scan Bluetooth Printers" onPress={scanBt} />
{printers.map(p => (
<Button key={p.address} title={`Print to ${p.name}`} onPress={() =>
print("bluetooth", p.address, [
{ type: "Init" },
{ type: "Text", value: "Hello via BLE!" },
{ type: "Cut" },
])
} />
))}
</View>
);
}| Runtime | TCP | Bluetooth | USB |
|---|---|---|---|
| Node.js | net (built-in) |
bluetooth-serial-port |
serialport |
| Bun | net (built-in) |
bluetooth-serial-port |
serialport |
| Electron | net (built-in) |
bluetooth-serial-port |
serialport |
| Expo (Android) | react-native-tcp-socket |
react-native-ble-plx |
Built-in native module |
| Expo (iOS) | react-native-tcp-socket |
react-native-ble-plx |
❌ |
| Browser | ❌ | ❌ | WebUSB + Web Serial (via "/web" entry) |
No configuration needed — just install the optional packages you need.
Each section is { type, value? }.
| type | value | Thermal | A4/PDF |
|---|---|---|---|
Init |
— | Reset printer | Ignored |
Text |
string |
Print text with newline | PDF text (auto-wrapped) |
Align |
"left" | "center" | "right" |
Horizontal alignment | ✓ |
Size |
{ width: 1-8, height: 1-8 } |
Text size multiplier | Font size scaled by max(w,h) |
Bold |
boolean |
Bold on/off | ✓ |
Underline |
boolean |
Underline | Ignored |
Italic |
boolean |
Italic | Ignored |
Invert |
boolean |
Reverse printing | Ignored |
Font |
"A" | "B" | "C" |
Font face | Ignored |
Rotate |
boolean |
90° rotation | Ignored |
UpsideDown |
boolean |
180° flip | Ignored |
Feed |
number (1-255) |
Feed n lines | Adds spacing |
FeedDots |
number (1-255) |
Feed n dots | Adds spacing |
Cut |
"full" | "partial" |
Paper cut | Ignored |
Line |
string |
Horizontal rule (40× char) | PDF horizontal rule |
Drawer |
2 | 5 |
Open cash drawer | Ignored |
Beep |
{ times, duration } |
Buzzer | Ignored |
CodePage |
number (0-255) |
Character page | Ignored |
Barcode |
{ data, barcode_type?, height?, width? } |
1D barcode | Ignored |
Qr |
{ data, size?, error_correction? } |
QR code | Ignored |
Table |
{ header, rows, separator?, headerBold?, borderAll?, gap?, columnWidths? } |
Formatted table | ✓ |
MultiColumn |
{ columns: [{text, width, align?}], gap? } |
Side-by-side columns | ✓ |
Image |
{ rasterData, bytesPerLine, height, algorithm? } |
NV bitmap image | ✓ (embedded) |
LineHeight |
number (0-255) |
ESC 3 n line spacing | Adds spacing |
LetterSpacing |
number |
Preview-only (no-op in ESC/POS) | ✓ |
ResetStyle |
— | ESC @ reset all styles | ✓ |
Note: Section types use PascalCase (
"Init","Text","Bold","Cut", etc.). Lowercase values will be silently ignored.
{ type: "Table", value: {
header: ["Item", "Qty", "Price"],
rows: [["Widget", "2", "$10"], ["Gadget", "1", "$20"]],
separator: "single", // "single" | "double" | "none" | "custom"
customSeparator: "~",
headerBold: true,
borderAll: false,
gap: 1,
columnWidths: [10, 5, 8]
}}{ type: "MultiColumn", value: {
columns: [
{ text: "Left column\nline 2", width: 15 },
{ text: "Right column", width: 15, align: "right" },
],
gap: 2
}}UPC-A UPC-E EAN13 EAN8 CODE39 ITF CODABAR CODE93 CODE128
| Value | Recovery |
|---|---|
L |
~7% |
M |
~15% (default) |
Q |
~25% |
H |
~30% |
Visual drag-and-drop template builder at:
https://thermal-print-builder.pages.dev
- Design receipts with live preview
- 25+ section types (text, barcode, QR, table, image, multi-column, cut, drawer, beep…)
- Export/import JSON templates
- Paper sizes: 58mm, 80mm, A4, Letter
- Theme support (dark/light)
- Run locally:
cd builder && bun dev
On Windows, the default USB printer driver (usbprint.sys) claims the interface and blocks WebUSB. The device may show as "Virtual PRN" — a virtual device created by the manufacturer's driver.
Fix with Zadig — replaces the OS driver with WinUSB:
- Download Zadig and run as Administrator
- Options → List All Devices (critical — printer won't appear in default mode)
- Select your printer from the dropdown (
USB Printing Support,Virtual PRN, orVID_0FE6&PID_811E) - In the right column, select WinUSB (not libusb-win32)
- Click Replace Driver, then restart the browser
After swapping to WinUSB, WebUSB can claim the interface. The printer won't appear in Windows Printers & Scanners (revert with Zadig anytime by selecting the original usbprint driver).
The device session is stuck. Unplug the printer, wait 5 seconds, plug it back in, then click Connect again in the browser.
macOS kernel driver (IOKit) has the interface. Remove the printer from System Settings → Printers & Scanners, then reconnect.
Only printers with USB-to-serial chips (CH340, CP2102, FTDI) appear as serial ports. Native USB printers won't show up here. Use the WebUSB transport with Zadig instead.
- Browser network printing — WebSocket proxy server to relay print jobs from browser to TCP network printers
- Live receipt preview — Render ESC/POS output visually in the browser before printing
- Cloudflare Worker print API — Deploy a Worker that accepts print jobs and forwards to network printers via tunnel
- Job queue / spooler — Queue multiple print jobs with retry and status
- Receipt templates — Built-in templates for invoices, orders, kitchen tickets
- Multiple paper sizes — 58mm, 80mm, 2-inch, 4-inch automatic formatting
- Better WebUSB error recovery — Handle device disconnect/reconnect gracefully during a print job
- React hooks —
usePrinter(),useUsbDevice(),useScale()for easy React integration
npx thermal-print <ip> <port>
# Sample receipt to network printer
bun print.ts
node --experimental-strip-types print.tssrc/
├── index.ts # Node.js/Bun entry
├── index.rn.ts # Expo/RN entry (dynamic imports)
├── index.web.ts # Browser entry (WebUSB + Web Serial)
├── cli.ts # CLI entry point
├── escpos.ts # ESC/POS command builder
├── pdf.ts # A4/PDF builder (pdf-lib)
├── env.d.ts # Type declarations for optional deps
└── transport/
├── tcp.ts # Node.js built-in net
├── bluetooth.ts # bluetooth-serial-port (Node.js)
├── bluetooth-expo.ts # react-native-ble-plx (Expo)
├── usb.ts # serialport (Node.js)
├── usb-expo.ts # Android USB native bridge
├── usb-web.ts # WebUSB transport (browser)
├── serial-web.ts # Web Serial transport (browser)
├── spooler.ts # CUPS / PowerShell spooler
├── detect.ts # Runtime detection
└── network-discovery.ts # mDNS + SNMP discovery
android/ # Android USB native module
plugin/ # Expo config plugin
builder/ # Visual template builder (Vite + React)
docs/ # Per-transport documentation
Contributions welcome! Please open an issue or submit a pull request.
MIT © 2026 Sandeep Kushwaha