Barcode Symbologies and the Math of Check Digits
A technical explainer on barcode symbologies and the mod-10 check digit algorithm, grounded in the loopaloo Barcode Generator's real implementation (jsbarcode ^3.12.1).
A barcode is not a picture of a number. It is a 1D encoding scheme, and the choice of scheme (the "symbology") determines what characters you can encode, how long the data can be, whether there is built-in error detection, and how a scanner finds the start and end of the message in a noisy image. Pick the wrong symbology and your data either will not fit or will not scan.
Most people meet barcodes through retail: the EAN-13 on a cereal box, the UPC-A on a North American product. But those are just two members of a much larger family, and they happen to be the most rigid ones. Code 128 and Code 39 encode arbitrary text. ITF encodes pairs of digits very densely for shipping cartons. Each makes a different trade between density, character set, and self-checking.
This post walks through the symbologies our barcode generator supports (it is built on the jsbarcode library, version ^3.12.1, per the project's package.json), explains what each one is actually for, and then does the part everyone glosses over: the mod-10 check digit arithmetic, worked by hand on a real EAN-13.
The symbologies, grouped by what they encode
The generator (src/components/tools/converter/BarcodeGenerator.tsx) groups its formats into three buckets in the UI: Standard, Retail/Product, and Industrial. That grouping maps cleanly onto how the symbologies differ.
| Symbology | Character set | Length | Check digit | Typical use |
|---|---|---|---|---|
| Code 128 | All 128 ASCII | Variable | Yes (mod-103, internal) | Shipping labels, logistics |
| Code 39 | A-Z, 0-9, a few symbols | Variable | Optional | Inventory, ID badges |
| Code 93 | Same set as Code 39, denser | Variable | Yes (two internal) | Compact labeling |
| EAN-13 | Digits only | Fixed (13) | Yes (mod-10) | Retail, international |
| EAN-8 | Digits only | Fixed (8) | Yes (mod-10) | Small retail packaging |
| UPC-A | Digits only | Fixed (12) | Yes (mod-10) | Retail, North America |
| UPC-E | Digits only | Fixed (compressed) | Yes | Small North American packages |
| ITF / ITF-14 | Digits only, even count | Variable / fixed 14 | Optional / yes | Cartons, shipping |
| MSI | Digits only | Variable | Optional (mod-10 or mod-11) | Warehouse shelving |
| Codabar | Digits and a few symbols | Variable | None standard | Libraries, blood banks |
| Pharmacode | Encodes an integer 3-131070 | Variable | None | Pharmaceutical packaging |
Two distinctions in that table drive almost every practical decision: character set and fixed-versus-variable length.
Variable length: Code 128 and Code 39
Code 128 is the workhorse for anything that is not a retail product. It encodes the full ASCII range across three internal code sets (A, B, and C), and the generator exposes those as CODE128A, CODE128B, and CODE128C in addition to the auto-switching CODE128. Code set C is the interesting one: it encodes digits two at a time, so a numeric string packs into half the symbols. That is why the generator's validator for CODE128C requires an even number of digits (/^\d+$/.test(s) && s.length % 2 === 0). If you feed it an odd count, there is no way to pair the final digit.
Code 39 is older and simpler. It encodes uppercase letters, digits, and a handful of symbols (-, ., space, $, /, +, %), each character represented by a fixed pattern of bars and spaces where three of the nine elements are wide ("3 of 9", hence the name). It is bulky compared to Code 128 but ubiquitous in legacy industrial and government systems because it is trivial to print and decode. Its check digit is optional and often omitted.
The key property of both: the data length is not baked into the symbology. You can encode A, or you can encode a 40-character serial number. The scanner uses dedicated start and stop patterns to know where the message begins and ends.
Fixed length: the retail family
EAN-13, EAN-8, UPC-A, and UPC-E are rigid. EAN-13 is always 13 digits. UPC-A is always 12. EAN-8 is 8. There is no "encode whatever you want" mode, because the structure is allocated: a number-system or country prefix, a manufacturer code, a product code, and a final check digit. The whole point of the global GS1 system is that the same 13 digits mean the same product everywhere, so the format cannot flex.
This rigidity shows up directly in the generator's validators. For EAN-13:
const validateEAN13 = (s: string): boolean => {
if (!/^\d{12,13}$/.test(s)) return false;
if (s.length === 12) return true; // Will auto-calculate check digit
const data = s.slice(0, 12);
const checkDigit = parseInt(s[12], 10);
return calculateMod10CheckDigit(data) === checkDigit;
};
Notice the two accepted lengths. If you pass 12 digits, the validator accepts it and jsbarcode computes the 13th at render time. If you pass all 13, it verifies that the last digit is the correct check digit and rejects the input if it is not. UPC-A behaves the same way with 11 or 12 digits, EAN-8 with 7 or 8, and ITF-14 with 13 or 14. That auto-calculate-or-verify behavior is the same pattern across the whole retail family.
The mod-10 check digit, worked by hand
Here is the algorithm that EAN-13, EAN-8, UPC-A, and ITF-14 all share. The generator implements it once:
const calculateMod10CheckDigit = (digits: string): number => {
let sum = 0;
const len = digits.length;
for (let i = 0; i < len; i++) {
const digit = parseInt(digits[i], 10);
// If position from the right is odd, multiply by 3
if ((len - i) % 2 === 1) {
sum += digit * 3;
} else {
sum += digit;
}
}
return (10 - (sum % 10)) % 10;
};
The rule in words: number the data digits from the right starting at 1. Multiply every odd-positioned digit by 3, leave every even-positioned digit as-is, sum it all up, then the check digit is whatever you need to add to reach the next multiple of 10.
Let me do it on the example value the generator ships for EAN-13: 5901234123457. The first 12 digits are the data; the final 7 is the check digit we want to confirm.
Data: 5 9 0 1 2 3 4 1 2 3 4 5
Number these from the right. Position 1 is the rightmost data digit (5), position 12 is the leftmost (5). Odd positions (1, 3, 5, 7, 9, 11) get the x3 weight; even positions get x1.
| Digit | Value | Position from right | Weight | Contribution |
|---|---|---|---|---|
5 (leftmost) | 5 | 12 | x1 | 5 |
9 | 9 | 11 | x3 | 27 |
0 | 0 | 10 | x1 | 0 |
1 | 1 | 9 | x3 | 3 |
2 | 2 | 8 | x1 | 2 |
3 | 3 | 7 | x3 | 9 |
4 | 4 | 6 | x1 | 4 |
1 | 1 | 5 | x3 | 3 |
2 | 2 | 4 | x1 | 2 |
3 | 3 | 3 | x3 | 9 |
4 | 4 | 2 | x1 | 4 |
5 (rightmost data) | 5 | 1 | x3 | 15 |
Sum: 5 + 27 + 0 + 3 + 2 + 9 + 4 + 3 + 2 + 9 + 4 + 15 = 83.
Now 83 mod 10 = 3, and the check digit is (10 - 3) % 10 = 7. That matches the 7 printed at the end of 5901234123457, so the barcode is valid. If a scanner misreads one digit, the weighted sum almost always lands on a different remainder, and the check digit will not match, so the read is rejected rather than silently accepted.
Why the outer % 10
The final (10 - (sum % 10)) % 10 has a subtle but important wrapper. If sum % 10 is already 0, then 10 - 0 = 10, which is not a single digit. The outer % 10 folds that back to 0. Without it, a data string whose weighted sum is a clean multiple of 10 would produce an invalid two-character "digit." This is the kind of off-by-one edge case that quietly breaks naive implementations.
UPC-A is the same math, shorter
UPC-A uses 11 data digits instead of 12, but the exact same weighting and the exact same function. That is why the generator can reuse calculateMod10CheckDigit for validateUPC, validateEAN8, and validateITF14 without modification. The only thing that changes is how many digits go in. (UPC-A is, in fact, structurally an EAN-13 with a leading zero, which is why a 12-digit UPC and a 13-digit EAN that starts with 0 represent the same product.)
MSI is a different mod-10
Worth flagging: MSI barcodes (MSI10, MSI1010, and friends in the generator) also call their scheme "mod 10," but it uses a different weighting (a doubling-and-digit-sum approach closer to the Luhn algorithm) and can chain two check digits. "Mod 10" names a family of techniques, not one specific calculation, so do not assume an MSI check digit and an EAN check digit are computed the same way. They are not.
Quiet zones: the part that is not bars
A barcode is useless without its quiet zone, the blank margin on either side of the bars. The scanner needs that clear runway to detect where the symbol starts and to set its black/white threshold. The GS1 spec asks EAN-13 for a left quiet zone of 11 modules and a right of 7; UPC-A wants 9 on each side. A "module" is the width of the narrowest bar, so the quiet zone scales with the printed size.
In the generator, the relevant knob is the margin option passed to jsbarcode, set to 10 in generateBarcode:
JsBarcode(canvas, data, {
format: fmt,
width: width, // module width in pixels
height: height,
margin: 10, // the quiet zone, in pixels
// ...
});
The practical failure mode here is real: people crop a barcode tight to the bars to save space, eliminate the quiet zone, and then the symbol will not scan even though every bar is perfect. If you are placing a generated barcode into a larger design, keep that white margin. The width option controls the module width in pixels (the generator's slider runs from 1 to 5), and the quiet zone should remain proportional to it.
What JsBarcode actually generates
A few things are worth being precise about, because it is easy to over-claim what a barcode tool does.
JsBarcode renders the symbology: it takes your data, applies the encoding rules for the chosen format, computes the format's own check digit where the standard defines one, and draws the bars (the generator draws to a <canvas> and then exports a PNG via canvas.toDataURL('image/png'), with an SVG download option that wraps that same raster in an <image> element). It is a generator, not a database. It does not assign you a real GS1 company prefix, does not check whether a product code is registered, and does not verify that your EAN-13 corresponds to an actual product. A barcode that scans correctly can still be commercially meaningless if the underlying number was not allocated to you by GS1.
The generator's own validators add a client-side layer on top: before handing data to JsBarcode, a useEffect runs formatInfo[format].validator(inputText) and shows an error like "Invalid input for EAN-13" if the character set or check digit is wrong. That is why you get immediate feedback when you type 13 digits with a bad final digit, rather than a confusing render. For UPC-E, the validator is deliberately permissive (it accepts 6 to 8 digits and lets JsBarcode handle the compression rules), because UPC-E's zero-suppression conversion is genuinely intricate and the library owns that logic.
Choosing a symbology, quickly
If you are deciding which format to generate:
- Encoding letters or mixed text, variable length: Code 128 (or Code 39 for legacy systems that require it).
- A retail product for sale, outside North America: EAN-13 (you need a real GS1 prefix for commercial use).
- A retail product in North America: UPC-A.
- Tiny package with no room for 13 digits: EAN-8 or UPC-E.
- A shipping carton holding many units: ITF-14.
- Internal warehouse or library tracking where any number works: MSI or Codabar.
The recurring theme: fixed-length retail symbologies buy you global uniqueness and a built-in check digit at the cost of flexibility, while variable-length symbologies buy you flexibility at the cost of needing your own validation discipline.
Try it
You can generate any of these formats, with the check digit auto-computed or verified, using the loopaloo Barcode Generator. Type 12 digits and watch it append the EAN-13 check digit, or type all 13 and confirm your math against the worked example above. If you have an existing barcode image and want to go the other direction (pull the digits back out and see which symbology it is), the loopaloo Barcode Reader decodes it. Together they cover both ends of the encode/decode loop without anything leaving your browser.
Related Tools
Related Articles
Try Our Free Tools
200+ browser-based tools for developers and creators. No uploads, complete privacy.
Explore All Tools