DEV Community

Jordan Vance
Jordan Vance

Posted on

Rendering scannable barcodes in the browser with JsBarcode (no backend)

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>
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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" });
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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)