Display

Barcode

A vector Code 128 / EAN-13 renderer — automatic subset switching, a real check digit, spec quiet zones, and a refusal that names what cannot be encoded.

Preview in your theme

Loading preview…

import * as React from "react"
import { TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"

export type BarcodeSymbology = "code128" | "ean13"

export interface BarcodeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
  /** Data to encode. Code 128 takes any ASCII string; EAN-13 takes 12 or 13 digits. */
  value: string
  /** Which linear symbology to draw. Default "code128". */
  symbology?: BarcodeSymbology
  /** Width of one narrow module (1X) in px — the whole symbol scales from it. Default 2. */
  moduleWidth?: number
  /** Bar height in px. EAN-13 guard bars extend below it into the digit line. Default 64. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/barcode.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "Barcode" component that draws a linear
(1-D) symbol as SVG geometry — Code 128 or EAN-13. lucide-react for the refusal
icon; no barcode library, no canvas.

Contract
- forwardRef<HTMLDivElement, BarcodeProps>. Props extend
  Omit<React.HTMLAttributes<HTMLDivElement>, "children"> and add:
  value: string (required), symbology?: "code128" | "ean13" (default
  "code128"), moduleWidth?: number (px per narrow module, the 1X, default 2),
  height?: number (bar height in px, default 64), showText?: boolean (default
  true), label?: string (accessible name). className merges onto the wrapper,
  every other native prop lands there too.
- No state, no effects, no browser APIs, no time reads: the symbol is a pure
  function of props. The file therefore carries no "use client", renders in a
  server component, prints, and never disagrees between SSR and hydration.
  Nothing is measured, so there is no observer, timer or listener to clean up.
- Also export ean13CheckDigit(first12: string): number. Label pipelines usually
  have to complete a 12-digit GTIN long before it reaches a component.
- Encoding returns a discriminated result: { ok: true, symbol } | { ok: false,
  reason }, and the component renders one of exactly two trees from it. The
  symbol is { bars, groups, width, text } expressed in *modules*, not pixels —
  bars are { x, width, guard } with x measured from the canvas edge, quiet
  zones included — so one multiply by moduleWidth produces every coordinate,
  and a second symbology can be added without touching the renderer.

Behavior — Code 128
- A table of 107 patterns indexed by code word value. Values 0-105 are six
  digits of bar/space widths that sum to 11 modules and always start with a
  bar; index 106 is the seven-element, 13-module stop pattern, which ends on a
  bar. Self-check while building the table: the bar widths of every pattern sum
  to an even number.
- Automatic subset selection, per the spec rather than "always B". Start in C
  when the value opens with four or more digits or is an all-digit string of
  even length; otherwise start in A when a control character occurs before the
  first lowercase letter, else in B. Then walk the value: in A or B, a run of
  four or more digits switches to C (an odd run spends its first digit in the
  current set so the remainder pairs up); in C, fewer than two digits left
  switches back to A or B by the same control-vs-lowercase test; a character
  the current set cannot hold (lowercase in A, a control character in B) flips
  to the other one. Every ASCII code point lives in A or B, so that flip always
  terminates — the loop can never spin.
- Check word = (start value + Σ position × value) mod 103, appended before the
  stop. It is what makes a misread self-evident to the decoder, so it is not
  optional, and it is the reason subset switches must be counted as positions.
- Refusals: an empty value, and any character above ASCII 127. The message
  names the offending character, because "it did not render" is not a
  diagnosis.

Behavior — EAN-13
- Strip spaces and hyphens, then accept exactly 12 digits (compute the 13th) or
  13 (verify it). The check digit weights the first twelve 1-3-1-3… and takes
  whatever is missing to the next multiple of ten.
- A 13-digit value whose check digit disagrees is refused, with both the given
  and the computed digit in the message: rendering it would produce a label
  that every scanner in the building rejects, which is worse than no label.
- The symbol is 95 modules: 101 guard, six left digits, 01010 centre guard, six
  right digits, 101 guard. The first digit is never drawn — it is carried by
  the odd/even (L/G) parity pattern of the six left digits, which is why a
  table of ten parity rows exists. Right digits use R, the complement of L, so
  every right pattern opens with a bar and the two halves cannot be confused
  when the symbol is scanned upside down.
- Guard bars are identified by module offset (0, 2, 46, 48, 92, 94) and drawn
  longer than the data bars, so the digit groups sit between them.

Rendering & styling
- One <svg> with viewBox="0 0 width height" plus matching width/height
  attributes and className "h-auto max-w-full": the symbol keeps its natural
  size, shrinks inside a narrow card, and stays sharp at any scale because it
  is geometry — a canvas would resample and hand the decoder grey edges.
- Quiet zones are geometry, not padding: 10X on both sides for Code 128, 11X
  left / 7X right for EAN-13, where the 11X exists to hold the lead digit.
  Container padding cannot be trusted for this, because a consumer will remove
  it and never know the symbol stopped scanning.
- A full-canvas <rect className="fill-background"> is the substrate; bars are
  <rect className="fill-current"> inside a <g shapeRendering="crispEdges">, so
  edges snap to device pixels instead of resolving into a grey half-pixel that
  fattens a bar for the decoder. Keep crispEdges off the <text>.
- The human-readable line is <text className="fill-current font-mono"> with
  textAnchor="middle" — one centred group for Code 128, three for EAN-13 (lead
  digit inside the left quiet zone, then 6 + 6 between the guards). Its font
  size is derived from the module, max(7, moduleWidth × 7), so text, bars and
  quiet zones scale as one object. Non-printable characters are still encoded
  but printed as "·", since a control byte in the caption is just tofu.
- Accessibility: the wrapper carries role="img" and an aria-label such as
  "Code 128 barcode, ZY-4417-A"; the <svg> is aria-hidden, because a screen
  reader needs the value, not sixty rectangles. A refusal drops role="img"
  entirely and renders the reason as real text beside an aria-hidden
  lucide TriangleAlert in text-destructive — a refusal that is only visual is
  not a refusal.
- Semantic tokens only: fill-background / bg-background, fill-current with
  text-foreground, border, text-destructive. There is no motion, so
  prefers-reduced-motion has nothing to suppress; that is a property of the
  design, not an omission.
- Contrast is the one physical constraint. Laser and CCD scanners expect dark
  bars on a light substrate; fill-current plus fill-background follows the
  theme, which is right on screen and wrong on a dark-mode phone held under a
  scanner. For a symbol that will be printed or scanned off the screen, pin the
  surrounding element to the light palette instead of hardcoding colours in the
  component.

Customization levers
- Density and footprint: moduleWidth is the only scale knob — bars, quiet zones
  and the text size all derive from it; height changes bar length alone. Keep
  1X at 1px or more on screen (0.264mm in print), which is the usual
  scannability floor.
- Sub-blocks: showText={false} removes the human-readable line and, with it,
  the EAN-13 guard extension; strip the wrapper's border and padding for a bare
  mark; or wrap the component in your own <figure> caption when the label needs
  typography the symbology does not define.
- Filling a container: the SVG keeps its natural width, so add [&_svg]:w-full
  to className when the symbol should stretch to the card instead of centring.
- More symbologies: write an encoder that returns the same { bars, groups,
  width, text } shape and the renderer needs no change — EAN-8 (67 modules),
  UPC-A (an EAN-13 with a leading zero) and ITF-14 all fit it.
- Quiet zones: the constants are spec minimums. Raise them when the label sits
  next to dark artwork; never lower them to win layout space.
- Tokens: the bars follow whatever text-* token the wrapper carries
  (text-foreground by default, text-primary for a branded packing slip), and
  the substrate follows fill-background — swap it for a card token when the
  label prints on tinted stock.

Concepts

  • Auto subset switching — Code 128 is three alphabets in one symbology, and picking between them is an encoding decision, not a prop: an all-digit run of four or more pays for itself in subset C (two digits per code word), a lowercase letter forces B, a control byte forces A, and an odd digit run spends its first digit before switching so the rest can pair up.
  • The check digit is a contract, not a formality — EAN-13 computes the 13th digit from the first twelve, and a 13-digit input is verified rather than trusted; the modulo-103 check word does the same job for Code 128. Both exist so that a misread announces itself instead of quietly becoming a different SKU.
  • Quiet zone as geometry — the clear space a scanner needs (10X for Code 128, 11X left / 7X right for EAN-13) is drawn inside the SVG rather than left to container padding, because padding is the first thing a consumer removes and the symbol would stop scanning without any visible change.
  • Refusal instead of a broken symbol — an unencodable character or a mismatched check digit produces a stated reason, not a blank box and not a best-effort drawing: a label that renders but cannot be scanned fails later, in a warehouse, where nobody can debug it.
  • Modules as the coordinate system — every measurement is expressed in modules and multiplied by moduleWidth exactly once at render time, which is what lets density, text size and quiet zones scale as a single object and lets a new symbology reuse the renderer untouched.
  • Vector, not canvas — bars are <rect>s in a viewBox, so the same element is a shelf strip at 1X = 1px and a poster at 1X = 8px; shapeRendering="crispEdges" keeps the edges from being antialiased into grey half-pixels that a decoder reads as a wider bar.

On This Page