react-sunmi-thermal-printer
v1.0.0
Published
Direct browser-to-printer printing for Sunmi thermal printers. React hooks and ESC/POS utilities for POS receipts, KOTs, invoices, and billing slips — no drivers, no middleware.
Maintainers
Readme
react-sunmi-thermal-printer
Direct browser-to-printer printing for Sunmi thermal printers — no drivers, no middleware, no desktop app.
A React library that solves one specific problem: sending print jobs directly from a web browser to a Sunmi thermal printer over your local network using HTTP.
Built for web-based POS systems, restaurant management platforms, and any browser app that needs to print receipts, KOTs, invoices, or billing slips on a Sunmi device.
The Problem This Solves
Most web-based POS systems struggle with printing because:
- Browsers don't support raw socket connections to printers
- Traditional print dialogs are slow and require user interaction
- Third-party print servers add infrastructure complexity
- Native drivers don't work inside a browser
Sunmi printers expose a local HTTP endpoint (http://<printer-ip>/cgi-bin/print.cgi) that accepts raw ESC/POS commands as a POST body. This package gives you React hooks and a command library to build and send those commands directly from your browser app — instant, silent, zero-click printing.
Use Cases
| Use Case | Description | |---|---| | KOT (Kitchen Order Ticket) | Print order items to kitchen printer on order placement | | POS Receipt | Print customer receipts with itemized billing, taxes, totals | | Invoice | Print A4-style or thermal invoices with full order details | | Billing Slip | Quick billing printout for cashier stations | | Void / Cancel Slip | Print voided item or cancelled order slips for kitchen | | Shift Report | Print end-of-shift or daily summary reports | | Connection Test | Verify printer is reachable and print a test receipt |
Installation
npm install react-sunmi-thermal-printerPeer dependency: React ≥ 17.0.0
Browser Requirement (One-Time Setup)
Because your web app is served over HTTPS but the Sunmi printer uses HTTP, Chrome blocks the request by default. You need to whitelist your printer's IP once:
- Open Chrome and go to:
chrome://flags/#unsafely-treat-insecure-origin-as-secure - Set the flag to Enabled
- Add your printer IP in the text field:
http://192.168.1.100 - Click Relaunch
The example demo app includes a built-in step-by-step setup guide with copy buttons.
Quick Start
import { usePrinter, printerCommands, resolveCharsPerLine, resolveDotWidth } from "react-sunmi-thermal-printer";
function MyPOS() {
const { print, isLoading } = usePrinter({
ip: "192.168.1.100",
serialNumber: "N4502482T0086",
paperWidth: "80mm",
copies: 1,
});
const printReceipt = async () => {
const totalChars = resolveCharsPerLine("80mm"); // 48
const dotWidth = resolveDotWidth("80mm"); // 576
const sep = (c = "-") => printerCommands.appendText(c.repeat(totalChars) + "\n");
// Define columns as dot-width slices: Qty(15%) | Item(55%) | Price(30%)
const colWidths = printerCommands.setColumnWidths(
printerCommands.columnWidthWithAlignment(Math.floor(dotWidth * 0.15), 0), // left
printerCommands.columnWidthWithAlignment(Math.floor(dotWidth * 0.55), 0), // left
printerCommands.columnWidthWithAlignment(Math.floor(dotWidth * 0.30), 2), // right
);
let hex = "";
hex += printerCommands.setAlignment(1);
hex += printerCommands.setFontSize("h7", true);
hex += printerCommands.appendText("MY RESTAURANT\n");
hex += printerCommands.setNormalFont();
hex += sep("=");
// Column header
hex += printerCommands.setAlignment(0);
hex += printerCommands.setFontSize("h8", true);
hex += printerCommands.printInColumns(colWidths, "QTY", "ITEM", "PRICE");
hex += printerCommands.setNormalFont();
hex += sep();
// Items — no manual spacing, columns handle alignment
hex += printerCommands.printInColumns(colWidths, "2x", "Classic Burger", "10.00");
hex += printerCommands.printInColumns(colWidths, "1x", "Caesar Salad", "8.50");
hex += sep();
// Totals
hex += printerCommands.setFontSize("h8", true);
hex += printerCommands.printInColumns(colWidths, "", "TOTAL:", "18.50");
hex += printerCommands.setNormalFont();
hex += sep("=");
hex += printerCommands.lineFeed(4);
hex += printerCommands.cutPaper(true);
await print(hex);
};
return <button onClick={printReceipt} disabled={isLoading}>Print Receipt</button>;
}Connection Config
| Field | Type | Default | Description |
|----------------|------------------------------|-----------|--------------------------------------|
| ip | string | "" | Printer IP address (required) |
| serialNumber | string | "" | Printer serial / SN number |
| copies | number | 1 | Default number of copies |
| paperWidth | "58mm" \| "80mm" \| string | "80mm" | Paper width — see Paper Width Guide |
| timeout | number | 5000 | Request timeout in ms |
| encoding | string | "UTF-8" | Text encoding |
Paper Width Guide
| Paper | Chars/line | Dot Width | Common Use |
|--------|-------------------------|-----------|-----------------------|
| 58mm | 32 | 384 | Small receipt printers|
| 80mm | 48 | 576 | Standard POS printers |
| Custom | Math.floor(mm * 0.53) | — | Pass as "72mm" etc. |
usePrinterStatus(config)
Checks printer reachability on demand — no auto-polling on mount. Call connect() yourself (e.g. on a save/connect button click).
const { isConnected, isLoading, error, connect } = usePrinterStatus({
ip: "192.168.1.100",
serialNumber: "N4502482T0086",
});
// Only fires when explicitly called:
<button onClick={connect}>Test Connection</button>Returns
| Field | Type | Description |
|---------------|----------------|---------------------------------------|
| isConnected | boolean | Whether the printer responded |
| isLoading | boolean | Connection check in progress |
| error | string\|null | Error message if unreachable |
| connect | () => Promise<void> | Trigger connection check — returns a Promise |
usePrinter(config)
Sends raw ESC/POS hex to the printer. Build any template using printerCommands.
const { print, isLoading, lastError, printerStatus } = usePrinter({
ip: "192.168.1.100",
serialNumber: "N4502482T0086",
paperWidth: "80mm",
copies: 1,
});Returns
| Field | Type | Description |
|-----------------|-----------------------------------|-------------------------------|
| print | (hex: string, copies?: number) => Promise<void> | Send ESC/POS hex to printer |
| isLoading | boolean | Print job in progress |
| lastError | string\|null | Last error message |
| printerStatus | object | Output of usePrinterStatus |
printerCommands
Stateless ESC/POS command builder. Every method returns a hex string — concatenate and pass to print().
import { printerCommands } from "react-sunmi-thermal-printer";
const hex =
printerCommands.setAlignment(1) + // center
printerCommands.setFontSize("h6", true) + // large bold
printerCommands.appendText("KOT #5\n") +
printerCommands.setNormalFont() +
printerCommands.separator("-", 48) +
printerCommands.setAlignment(0) + // left
printerCommands.appendText("2x Burger\n") +
printerCommands.lineFeed(4) +
printerCommands.cutPaper(true);
await print(hex);Available Commands
| Method | Parameters | Description |
|--------|-----------|-------------|
| appendText(str) | str: string | Encode text to UTF-8 ESC/POS hex |
| lineFeed(n?) | n: number = 1 | Feed n lines |
| cutPaper(fullCut) | fullCut: boolean | Full or partial paper cut |
| setAlignment(n) | n: 0\|1\|2 | 0=left, 1=center, 2=right |
| setFontSize(size, bold?) | size: "h1"–"h8"\|"" | Font scale + optional bold |
| setBoldFont(enabled) | enabled: boolean | Toggle bold only |
| setNormalFont() | — | Reset font to default |
| separator(char, length) | char: string, length: number | Print a separator line |
| darkBackground(text) | text: string | Inverted black background text |
| setColumnWidths(...values) | ...encoded: number[] | Returns column widths array for dot-based columns |
| columnWidthWithAlignment(width, align) | width: number, align: 0\|1\|2 | Encode dot-width + alignment |
| printInColumns(widths, ...texts) | widths: number[], texts: string[] | Print text in dot-positioned columns |
| setAbsolutePrintPosition(n) | n: number | Set horizontal print position in dots |
Dynamic Column Helper
Define columns as percentage widths — they auto-resolve to character counts based on paper width. No dot math needed.
import { formatColumns, buildColumnLine, resolveCharsPerLine } from "react-sunmi-thermal-printer";
// Define columns as % of paper width
const columns = [
{ label: "Qty", width: 15, align: "left" },
{ label: "Item", width: 60, align: "left" },
{ label: "Price", width: 25, align: "right" },
];
// Resolve to actual char widths for 80mm (48 chars/line)
const resolved = formatColumns(columns, "80mm");
// → [{charWidth: 7}, {charWidth: 28}, {charWidth: 12}]
// Build a padded text line
const line = buildColumnLine(["2x", "Classic Burger", "12.99"], resolved);
// → "2x Classic Burger 12.99"
// Use in a receipt
let hex = "";
hex += printerCommands.setAlignment(0);
hex += printerCommands.appendText(buildColumnLine(resolved.map(c => c.label), resolved) + "\n");
hex += printerCommands.separator("-", resolveCharsPerLine("80mm"));
items.forEach(item => {
hex += printerCommands.appendText(
buildColumnLine([`${item.qty}x`, item.name, item.price.toFixed(2)], resolved) + "\n"
);
});Helper Functions
| Function | Description |
|---|---|
| formatColumns(columns, paperWidth) | Resolve % widths → { charWidth, align, label }[] |
| buildColumnLine(cells, resolvedCols) | Build a padded string line from cell values |
| resolveCharsPerLine(paperWidth) | Get total characters per line for a paper width |
| resolveDotWidth(paperWidth) | Get total dot width for ESC/POS absolute positioning |
| padCell(text, width, align) | Pad a string to exactly N characters |
Building a KOT Template (Example)
The package doesn't include built-in templates — you build them with printerCommands. Here's a minimal KOT:
import {
printerCommands,
formatColumns,
buildColumnLine,
resolveCharsPerLine,
} from "react-sunmi-thermal-printer";
export function buildKOT({ storeName, kotNumber, tableNumber, items, paperWidth = "80mm" }) {
const totalChars = resolveCharsPerLine(paperWidth);
const sep = (c = "-") => printerCommands.appendText(c.repeat(totalChars) + "\n");
const cols = formatColumns(
[
{ label: "Qty", width: 20, align: "left" },
{ label: "Item", width: 80, align: "left" },
],
paperWidth
);
let hex = "";
// Header
hex += printerCommands.setAlignment(1);
hex += printerCommands.setFontSize("h7", true);
hex += printerCommands.appendText(`${storeName}\n`);
hex += printerCommands.setNormalFont();
hex += sep("=");
// KOT info
hex += printerCommands.setFontSize("h6", true);
hex += printerCommands.appendText(`KOT No: ${kotNumber}\n`);
hex += printerCommands.setNormalFont();
if (tableNumber) {
hex += printerCommands.appendText(`Table: ${tableNumber}\n`);
}
hex += sep();
// Items
hex += printerCommands.setAlignment(0);
items.forEach((item) => {
hex += printerCommands.setFontSize("h7", true);
hex += printerCommands.appendText(
buildColumnLine([`${item.qty}x`, item.name], cols) + "\n"
);
hex += printerCommands.setNormalFont();
if (item.notes) {
hex += printerCommands.appendText(` Note: ${item.notes}\n`);
}
});
hex += sep("=");
hex += printerCommands.lineFeed(4);
hex += printerCommands.cutPaper(true);
return hex;
}Then use it:
const { print } = usePrinter({ ip: "192.168.1.100", serialNumber: "N4502482T0086" });
await print(buildKOT({
storeName: "My Kitchen",
kotNumber: "42",
tableNumber: "T3",
items: [
{ qty: 2, name: "Burger", notes: "No onion" },
{ qty: 1, name: "Fries" },
],
paperWidth: "80mm",
}));Running the Demo App
The example/ folder is a full Vite + React demo that shows every package function in action.
# 1. Build the package
cd react-sunmi-thermal-printer
npm install && npm run build
# 2. Run the demo
npm run example
# or:
cd example && npm install && npm run devDemo runs at http://localhost:5173 and includes:
- Connection panel with saved settings display + Chrome setup guide
- 7 function demo print buttons (alignment, font sizes, columns, column helper, dark background, separators, full receipt)
- Custom raw print builder with live code preview
- Live column helper character-width preview
- Full package exports reference
Publishing
Auto (GitHub Actions)
Push a version tag — the workflow builds and publishes to npm automatically:
git tag v1.0.1 && git push origin v1.0.1Add NPM_TOKEN in your GitHub repo → Settings → Secrets.
Manual
./scripts/publish.sh patch # or: minor / majorBuilds, bumps version, commits, tags, publishes, and pushes.
Contributing
- Fork the repo and create a feature branch
- Make changes in
src/ npm run buildto verify- Test with
npm run example - Open a pull request
