I had a small feature to add to a web app last month: let a user type a SKU and get back a Code 128 barcode they could print on a label. My first instinct was to reach for a barcode microservice or one of those GET /barcode?text=... image APIs. Then I remembered the whole thing can run on the client. No server, no rate limit, no data leaving the page.
The library that does the heavy lifting is JsBarcode. It's old, tiny, and it just works. Here's what I learned wiring it up for Code 128, EAN-13, and UPC-A.
The 30-second version
JsBarcode takes a target element (an <svg>, <canvas>, or <img>) and a value, and draws the barcode into it:
<svg id="bc"></svg>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script>
JsBarcode("#bc", "ABC-12345", { format: "CODE128" });
</script>
That's a complete, scannable Code 128 barcode. It encodes any ASCII string, which is why it's the default for internal SKUs, asset tags, and anything that isn't a retail product.
Render to SVG, not canvas (for print)
This is the bit I got wrong first. If you render to <canvas> and the user prints it, the bars get resampled and a cheap scanner can choke on the blurry edges. SVG stays crisp at any size because it's vector. So I render to SVG for anything that will be printed:
JsBarcode("#bc", value, {
format: "CODE128",
width: 2, // bar width in px (the module width)
height: 80,
displayValue: true,
margin: 10,
});
width here is the narrowest bar ("module") width, not the total image width — bumping it from the default 2 to 3 or 4 gives a more forgiving scan target on a low-res printer.
EAN-13 and UPC-A: the check-digit gotcha
Retail barcodes aren't free text. EAN-13 is 13 digits where the last one is a checksum, and UPC-A is the 12-digit North American subset. The mistake everyone makes is typing all 13 digits and getting an "invalid" error because their hand-typed check digit is wrong.
JsBarcode will compute the check digit for you. Give it the first 12 digits for EAN-13 (or 11 for UPC-A) and it appends the correct one:
// EAN-13: pass 12 digits, JsBarcode adds the 13th (checksum)
JsBarcode("#bc", "590123412345", { format: "EAN13" });
// UPC-A: pass 11 digits, JsBarcode adds the 12th
JsBarcode("#bc", "03600029145", { format: "UPC" });
If you pass the full count and the check digit is wrong, it throws. Wrap the call so a bad input doesn't blank the page:
try {
JsBarcode("#bc", value, { format: "EAN13" });
} catch (e) {
showError("That doesn't look like a valid EAN-13.");
}
By default an invalid value just clears the element silently, which is a confusing UX. Set valid: (v) => {...} or catch as above so you can actually tell the user what's wrong.
Letting users download a PNG
SVG is great on screen, but people want a file they can drop into a label template, and PNG is the lowest-friction format. The trick is to render the barcode into an offscreen <canvas> and pull a data URL:
function downloadPng(value) {
const canvas = document.createElement("canvas");
JsBarcode(canvas, value, { format: "CODE128", width: 2, height: 80 });
const a = document.createElement("a");
a.href = canvas.toDataURL("image/png");
a.download = `${value}.png`;
a.click();
}
Because the canvas never gets attached to the DOM, the user just sees the SVG; the canvas exists for the half-millisecond it takes to encode the PNG. For crisper PNGs on hi-DPI displays, multiply width/height by window.devicePixelRatio before encoding.
Why client-side at all
Once I had this working I couldn't think of a reason to put it on a server. The input never leaves the browser (handy if the "SKU" is actually something sensitive), there's nothing to rate-limit or pay for, and it works offline. The only thing a backend buys you is barcode generation for non-browser clients — and if you're rendering for a human, you don't have one.
If you just want to punch in a value and grab an SVG/PNG without wiring up the library yourself, I put a no-signup version online while building this — the Code 128 generator runs entirely in the browser (same JsBarcode-under-the-hood approach, nothing uploaded). Useful for a one-off label or for checking that a value encodes the way you expect before you write the code.
Gotchas worth knowing
- Code 128 has three subsets (A/B/C). JsBarcode auto-switches, including the Code C numeric-pair compression, so long digit strings come out shorter than you'd expect. You don't manage this yourself.
-
Quiet zone matters. Keep
margin>= 10px. Scanners need the white space on either side; a flush-cropped barcode fails to read more often than people think. -
EAN-8 / ITF-14 exist too. Same API, different
format. EAN-8 for tiny packages, ITF-14 for shipping cartons.
That's the whole thing — a label-printing feature with zero backend. JsBarcode is one of those libraries that's been quietly correct for a decade.
Top comments (0)