EAN-13 barcode renderer in SVG
Compute the EAN-13 check digit and draw the bars as a dependency-free SVG React component.
React / Next.jsUtilitiesbarcodeean-13svgreact
Live preview
Code
// Left-hand "odd parity" patterns; R is the inverse, G is R reversed.
const L = ['0001101', '0011001', '0010011', '0111101', '0100011', '0110001', '0101111', '0111011', '0110111', '0001011'];
const R = L.map((p) => [...p].map((b) => (b === '1' ? '0' : '1')).join(''));
const G = R.map((p) => [...p].reverse().join(''));
// Which set encodes each of the six left digits, chosen by the first digit.
const PARITY = ['LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL'];
export function checkDigit(first12: string): number {
const sum = [...first12].reduce((s, d, i) => s + Number(d) * (i % 2 ? 3 : 1), 0);
return (10 - (sum % 10)) % 10;
}
export function encodeEan13(input: string): { bits: string; digits: string } {
if (!/^\d{12,13}$/.test(input)) throw new Error('EAN-13 needs 12 or 13 digits');
const body = input.slice(0, 12);
const digits = body + checkDigit(body);
if (input.length === 13 && input !== digits) throw new Error('Check digit mismatch');
const parity = PARITY[Number(digits[0])]!;
let bits = '101';
for (let i = 1; i <= 6; i++) bits += (parity[i - 1] === 'L' ? L : G)[Number(digits[i])];
bits += '01010';
for (let i = 7; i <= 12; i++) bits += R[Number(digits[i])];
return { bits: bits + '101', digits };
}
export function Ean13({ code, moduleWidth = 2, height = 90 }: { code: string; moduleWidth?: number; height?: number }) {
const { bits, digits } = encodeEan13(code);
const quiet = 9;
const bars: { x: number; w: number }[] = [];
for (let i = 0; i < bits.length; i++) {
if (bits[i] !== '1') continue;
const last = bars[bars.length - 1];
if (last && last.x + last.w === i) last.w += 1; // merge runs of adjacent bars
else bars.push({ x: i, w: 1 });
}
const width = (bits.length + quiet * 2) * moduleWidth;
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height + 18} role="img" aria-label={`Barcode ${digits}`}>
<rect width={width} height={height + 18} fill="#fff" />
{bars.map((b) => (
<rect key={b.x} x={(b.x + quiet) * moduleWidth} y={0} width={b.w * moduleWidth} height={height} fill="#000" />
))}
<text x={width / 2} y={height + 14} textAnchor="middle" fontFamily="monospace" fontSize={13} letterSpacing={3}>
{digits}
</text>
</svg>
);
}
How it works
What it solves
Retail barcodes look simple, but a correct EAN-13 needs a check digit, a parity pattern for the left half and exact guard bars. Most projects install a barcode library for one format; the encoding is small enough to write directly.
How it works
checkDigit sums the first twelve digits with alternating weights of one and three and returns the amount needed to reach the next multiple of ten. The encoder then builds a 95-module bit string: a start guard 101, six left digits, a centre guard 01010, six right digits and an end guard 101.
The first digit is not drawn; it selects which of the six left digits use the odd-parity L set and which use the even-parity G set. That is what the PARITY table holds. G codes are the right-hand R codes read backwards, and R codes are the bitwise inverse of L, so only the L table is stored and the others are derived. Each 1 becomes a bar; runs of ones are merged into one rectangle to keep the SVG small.
The component adds a quiet zone of nine modules on each side and prints the human-readable digits beneath using a monospace font.
Adapting it
- Accept 12 digits and append the check digit automatically, as done here.
- Scale with the
moduleprop for different print sizes. - Add a second encoder (Code 39 is similarly small) behind a
typeprop.
Pitfalls
- Reject non-numeric input before encoding.
- Print at least 80 percent of nominal size or scanners may fail.
How to use it
- Save as
components/Ean13.tsx(no dependencies, works on the server). <Ean13 code="590123412345" />draws the code and appends the check digit.- Pass
moduleWidth={2}to enlarge it. - If you pass 13 digits the last one is validated and a mismatch throws.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.