universal-thermal-printer
v0.7.5
Published
Print to thermal (ESC/POS) and A4 (PDF) printers via TCP, Bluetooth, USB, or system spooler — works in Node.js, Bun, Electron, and Expo
Maintainers
Keywords
Readme
universal-thermal-printer
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.
Install
npm install universal-thermal-printerRequirements
| 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) |
Dependencies
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-socketAPI
import { print, listBluetoothPrinters, listUsbPrinters, listSpoolerPrinters, listNetworkPrinters } from "universal-thermal-printer";
import type { PrintSection } from "universal-thermal-printer";Browser entry
import { print, requestUsbDevice, listUsbPrinters, requestSerialDevice } from "universal-thermal-printer/web";WebUSB and Web Serial transports are available via the "/web" sub-path export.
print(type, address, sections, options?)
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.
listBluetoothPrinters()
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.
listUsbPrinters()
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) |
listNetworkPrinters()
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 |
listSpoolerPrinters()
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 |
Examples
Network print
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" },
]);Bluetooth print
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" },
]);USB print (Node.js)
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 });USB print (Expo Android)
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" },
]);A4 / PDF print
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",
});Browser WebUSB print
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).
System spooler print
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 / React Native
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.
Quick start
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 buildExample component
import { 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 auto-detection
| 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.
PrintSection Reference
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.
Table
{ 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]
}}MultiColumn
{ type: "MultiColumn", value: {
columns: [
{ text: "Left column\nline 2", width: 15 },
{ text: "Right column", width: 15, align: "right" },
],
gap: 2
}}Barcode types
UPC-A UPC-E EAN13 EAN8 CODE39 ITF CODABAR CODE93 CODE128
QR error correction
| Value | Recovery |
|-------|----------|
| L | ~7% |
| M | ~15% (default) |
| Q | ~25% |
| H | ~30% |
Template Builder
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
Browser Troubleshooting
WebUSB on Windows — "Could not claim the printer interface" / "Virtual PRN"
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).
WebUSB on Windows — "Failed to execute 'open' on 'USBDevice': Access denied"
The device session is stuck. Unplug the printer, wait 5 seconds, plug it back in, then click Connect again in the browser.
WebUSB on macOS — "Could not claim the printer interface"
macOS kernel driver (IOKit) has the interface. Remove the printer from System Settings → Printers & Scanners, then reconnect.
Web Serial doesn't show any ports
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.
Next Features (Planned)
- 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
CLI
npx thermal-print <ip> <port>
# Sample receipt to network printer
bun print.ts
node --experimental-strip-types print.tsProject Structure
src/
├── 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 documentationContributing
Contributions welcome! Please open an issue or submit a pull request.
License
MIT © 2026 Sandeep Kushwaha
