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.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/barcode.jsonPrompt
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
moduleWidthexactly 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 aviewBox, 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.
Avatar Status
One avatar carrying its own state — initials derived from the name, a presence ring whose word joins the accessible name, a corner badge slot, five sizes and a circle or squircle frame.
World Clock
Several time zones on one card — rows ticking on one shared minute timer, with Intl-derived offsets and DST, yesterday/tomorrow markers, and a working-hours band that shows the overlap with your own day.