vector-labels
v0.1.0
Published
React components for technical labelling: barcodes, QR, DataMatrix, contour maps, stamps and 466 pictograms, all generated as SVG. Ships the free tier of the collection.
Maintainers
Readme
Vector Labels
React components for technical labelling: barcodes, QR, DataMatrix, contour maps, stamps and 466 pictograms, all generated as SVG. No images, no external fonts.
Aesthetic: industrial and logistics labelling. In the lineage of techwear, FUI and design fiction.
Two ways in. Pick the one that fits your project, the components are identical.
Install
As a package
npm i vector-labelsimport { Label, Row, Cell, Barcode } from "vector-labels";
import DataSheet from "vector-labels/blocks/data-sheet";
import "vector-labels/styles.css";Nothing else to set up. React is the only peer dependency.
As copied source
The shadcn CLI copies the source into your project, so you own the code and carry no dependency at all.
npx shadcn@latest add https://vectorlabels.com/r/vector-labels.jsonThen import the stylesheet once, in your global CSS:
@import "../components/vector-labels/vector-labels.css";A block installs the same way:
npx shadcn@latest add https://vectorlabels.com/r/data-sheet.jsonWith a namespace
Declare the registry once in components.json, then install by short name:
{
"registries": {
"@vector-labels": "https://vectorlabels.com/r/{name}.json"
}
}npx shadcn@latest add @vector-labels/data-sheetThe namespace also makes the components discoverable by agents such as Claude Code and Cursor, through the shadcn MCP server:
npx shadcn@latest mcp initUsage
import { Barcode, Cell, Glyph, Label, QrCode, Row, Text } from "@/components/vector-labels";
export function ShippingLabel() {
return (
<Label>
<Row>
<Cell>
<Text variant="h2">MHV-RST</Text>
<Text dim>RAAVH74 / 2025</Text>
</Cell>
<Cell fixed middle>
<Glyph name="fragile" size={20} />
</Cell>
</Row>
<Row noBorder>
<Cell flush>
<Barcode seed={42} height={34} />
</Cell>
<Cell fixed middle>
<QrCode seed={7} size={44} />
</Cell>
</Row>
</Label>
);
}What you get
No runtime dependency, no images, no fonts. Tailwind is not required: the stylesheet is standalone, every class is prefixed vl- and every custom property --vl-, so nothing can clash with your design system.
Everything renders on the server: generation is deterministic, so there is no hydration mismatch. No "use client" in the core, and a build guardrail enforces it.
Defaults first, then customise what you want
Every block renders complete with no props at all. The defaults are the design: the exact wording, pictograms and seeds of the original sheet.
<DataSheet /> {/* the full label, as drawn */}
<HandlingPanel />
<DispatchStub />Then you override only what you need, one prop at a time. Everything you do not pass keeps its default.
<HandlingPanel heading="COLD CHAIN" /> {/* one word changed */}
<HandlingPanel heading="COLD CHAIN" seed={777} /> {/* and new codes */}A build guardrail checks that every declared prop carries a default, so a block can never render a hole the first time someone drops it in. Today: 1,571 props across 337 blocks, every one of them with a default.
Customising the copy
Every block exposes all of its content as props. No text is frozen, and you never have to edit the copied file to change a wording.
<HandlingPanel
heading="COLD CHAIN"
reference="NF H00-060"
items={[
{ name: "temp", caption: "2 TO 8 °C" },
{ name: "nosun", caption: "KEEP SHADED" },
{ name: "fragile", caption: "FRAGILE" },
{ name: "thisway", caption: "THIS WAY UP" },
]}
body="LOT 44-A · PALLET 12"
seed={777}
/>One vocabulary for the whole collection
Every label has its own drawing, but the roles of the content repeat. So they carry the same name everywhere:
| Prop | Role |
|---|---|
| eyebrow | Micro code above the title |
| heading | Main title |
| subheading | Secondary title |
| caption | Label that introduces a piece of content |
| body | Block of technical mentions |
| footer | Bottom-of-label mention |
| reference | Reference code |
| value | Highlighted value |
| glyph / glyphs | Single pictogram / strip |
| fields | Label / value pairs, optionally checkable |
| items | Captioned pictograms |
| ticks | Values aligned without labels |
| table | Dense table |
| seed | Generation seed |
A block declares the subset it uses, and TypeScript rejects anything outside the contract:
export type ClassifiedCutProps = BlockProps<"heading" | "caption" | "value" | "glyph" | "seed">;When a label has a zone that is genuinely its own, its prop builds on a shared root: stubBody, codeReference, footerTicks, handlingGlyphs. Never an invented synonym.
In practice: you learn fifteen names, not three hundred. heading is the title in all 337 labels, seed the seed everywhere.
Three rules, enforced by a build guardrail
- Every text is a prop typed
React.ReactNode, so you can pass a string, JSX, or a value straight from your database. - Repetitions are arrays: pictograms, columns, table rows, check items. Put in three or ten, the layout follows.
- One seed per label.
seeddrives every barcode, QR and texture in the block at once. A different number per item, and each label is unique while staying reproducible.
Each block also exports its type, HandlingPanelProps, and forwards the Label props: className, style, ref, rounded, thick, cut.
Visual knobs
Visual customisation goes through typed, closed props, never a catch-all. They are available on every block, since each one forwards the Label options.
<DataSheet scale={1.6} /> {/* whole label, x1.6 */}
<DataSheet density="compact" /> {/* tight padding */}
<DataSheet tokens={{ ink: "#7d2b1f", paper: "#f6efe7" }} /> {/* local palette */}scale scales the whole label: typography, barcodes, textures, pictograms and inline styles. One number, no proportion drift. Implemented with zoom, which affects layout, unlike transform which would leave a hole the size of the original.
density is compact, default or roomy. Only cell padding changes, typography stays put, so information density varies without distorting the drawing.
tokens is a closed record: ink, paper, bg, accent, accent2, line, radius, font. Nothing else is accepted, and the scope is limited to that label.
palette names one of twenty colourways, and surface decides whether the label carries a background, a border, both or neither. paperOpacity fades the background alone, so a label can sit over an image and stay readable.
Anything outside these knobs is done in CSS through the data-slot attributes. That is deliberate: a prop that accepted anything would make the contract worthless and make every package update a breaking change.
The validator rejects, in a block's props, any index signature, any any, and any Record<string, ...>.
Styling from the outside
Every part carries a data-slot attribute. You can restyle any internal element from your own CSS, without ever editing the package files and without breaking future updates.
/* every cell */
[data-slot="vl-cell"] { padding: 8px 10px; }
/* only the oversized titles */
[data-slot="vl-text"][data-variant="mega"] { letter-spacing: -0.08em; }
/* one specific pictogram */
[data-slot="vl-glyph"][data-glyph="danger"] { color: #c0392b; }Available slots: vl-label, vl-row, vl-cell, vl-invert-bar, vl-sheet, vl-text, vl-spine, vl-field, vl-glyph, vl-glyph-strip, vl-glyph-grid, vl-barcode, vl-qr-code, vl-data-matrix, vl-stack-code, vl-circular-code, vl-stamp, vl-topo-map, vl-signal-trace, vl-histogram, vl-scatter, vl-dither, vl-halftone, vl-gauge, vl-ruler, vl-table, vl-check-row, plus the graphic marks.
Every component, generators and pictograms included, accepts className, style, ref, id and the data-* and aria-* attributes. className is merged, never overwritten.
Conversely, you never need to write a vl- class yourself: every class has its component. The shipped blocks are the proof, and a build guardrail forbids any internal class in their code.
The chassis
A label is a stack of Row, each Row a series of Cell. Internal borders close themselves on the last row and the last cell.
| Component | Role |
|---|---|
| Label | The frame. Options: surface (contained, flat, outline, none), paperOpacity, rounded, thick, cut (one bevelled corner), hud (four) with hudCorners, pill (stadium), notch (six die-cut silhouettes), tagged (point and eyelet), span |
| Row | A row. perforated for a dashed tear-off, noBorder |
| Cell | A cell. fixed, flush, tight, invert, middle, stack, width, grow |
| InvertBar | Inverted cell, same options as Cell |
| Between Inline Stack | Layout inside a cell |
| Leader | Dotted leader line |
| Sheet | Lays a series of labels out as a multi-column sheet |
| Text | All the typography, from micro (6px) to mega (62px), plus stencil, outline, halftone, glitch, oblique |
The generators
All deterministic: the same seed always draws the same artwork.
| Component | Draws |
|---|---|
| Barcode | Linear barcode. dense for the tight version, fill to fill a cell, vertical for a spine |
| StackCode | Stacked code, PDF-417 style |
| QrCode | QR with real finder and timing patterns |
| DataMatrix | Solid L and alternating border |
| CircularCode | Radial barcode, polar sectors |
| Stamp | Round stamp, text curved on a textPath |
| TopoMap | Contour lines, sum of polar sinusoids |
| SignalTrace | Signal trace on an oscilloscope grid |
| Histogram | Histogram |
| Scatter | Scatter plot, crosses and dots |
| Dither | 1-bit dithered ramp |
| Halftone | Halftone screen |
| ColorBars | Broadcast test card |
| RegistrationTarget | Press registration mark, one pass per ink |
| InkSet | Row of ink chips with names and figures |
| ThermalMap | Contour map filled with a heat ramp |
| Duotone | Two-ink halftone, printed out of register |
| HexField | Honeycomb panel |
| NodeGraph | Points linked to a spine |
| RadialSpokes | Hub with spokes ending in rings |
| SemiScale | Graduated half circle |
| EcgTrace | Cardiac trace, one complex repeated |
| CircuitTrace | Board runs dropping to terminators |
| Skyline | Vertical bars, some solid, some ruled |
| Candlestick | Open, high, low, close |
| FlowDiagram | Boxes hung off a bus |
| GlitchBars | Slabs of data torn along the scan line |
| ArcText | A line of type bent over a circle |
| SymbolMosaic | Wall of marks at varying density |
| AsciiArt | A mass resolved as characters |
| BlobMap | Filled organic islands joined by a route |
| AttitudeBall | Artificial horizon |
| DitherImage | A mass resolved as a point cloud |
| WireSphere | Sphere drawn as a cage of ellipses |
| RoomGrid | One-point room, floor, ceiling and walls |
| CurveChart | Decay curves on one axis |
| WaveChart | One wave on a ruled field |
| WarpGrid | A ruled mesh bent by a wave |
| WarpEllipse | An ellipse meshed and squeezed |
| ContourLines | Open section cuts across a frame |
| PixelDingbat | A mark on a coarse pixel field, mirrored |
| SpiralCoil | Flat spiral wound from the centre |
| DitherField | Point field driven from one edge |
| ColorField | Soft field of light, blown from the two signal colours |
| HeatBars | Histogram whose bars are coloured by their own value |
| PixelGrid | Mosaic of colour, one cell per square |
| Sunburst | Colour rosette, the test chart of a projector |
| ArcGauge | Half-dial, a coloured scale and its needle |
| Waveform | Recording trace, bars mirrored on a centre line |
| BrailleRow | Braille cells spread edge to edge |
| TunnelGrid | One-point perspective corridor |
| Dial | Graduated dial with a curved caption |
| HalftoneSphere | Sphere screened in dots, lit from the top left |
| FadeBar | Ink to nothing ramp, the printer's density wedge |
| SpectrumBar | Colour band, the one place the collection allows colour |
| Brush | Rough brush stroke behind a value |
The pictograms
466 glyphs in a 24x24 viewBox, drawn in currentColor, across 26 families: handling, compliance, recycling, arrows, asterisks, globes, shapes, technical registration, electrical, postal, head-up interface, quality seals, mecha decal, protection and access, telemetry, body and object, terminal and instrument, editorial, crosses and streetwear, road and equipment, care and compliance, head-up marks, media controls, film and format, solids and rosettes, pixel marks.
<Glyph name="globe-tilt" size={24} />
<GlyphStrip names={["ce", "weee", "mobius"]} spread />
<GlyphGrid columns={5} items={[{ name: "ce", caption: "CE" }]} />The repetitive families are generated, not drawn: one helper produces the 8 arrow directions from a single path, another every flower, a third every asterisk.
Marks and instruments
Bar, Rule, Dot, Square, Pill, Oval, Segments, XBox, Hatch, DotField, PlusGrid, StripeBar, Punches, Gauge, Ruler, DataTable, CheckRow, LeaderRow, Checker, Diamond, Redacted, Dialog, MenuBar, NotchBanner, TeethBar, RatingRow, ChainRings, SlantBar, TabFrame, StripeField.
Palettes
Twenty named colourways. Every block takes one, since palette is a Label prop and every block forwards them.
<HazardTape /> {/* its own default */}
<AssetTag palette="blueprint" /> {/* any block, any palette */}
<AssetTag palette="riso" tokens={{ accent: "#00d24a" }} /> {/* override one token */}| Name | Reads as |
|---|---|
| hazard | Signal yellow and black |
| safety | Hi-vis orange |
| blueprint | White line on engineering blue |
| riso | Two fluorescent inks on uncoated stock |
| thermal | Infrared: hot orange on a cold ground |
| terminal | Green phosphor |
| chrome | Cold industrial grey, electric blue signal |
| vapor | Night neon, magenta and cyan |
| acid | Acid green flyer stock |
| magma | Furnace: embers on soot |
| ocean | Deep water, lit from below |
| dust | Desert survey: tan stock, oxide red |
| frost | Cold light on white |
| punch | Loud pink, louder yellow |
| alarm | Fire red |
| exit | Escape green |
| mecha | Model kit decal: warm grey, caution yellow, warning red |
| bios | Setup screen: bright teal on a warmed-up tube |
| signal | Editorial sheet: near-white stock, one signal orange |
| nasa | Agency field: black on a full sheet of signal orange |
A palette is nothing but a set of tokens, so it can never do more than the tokens allow. It states its colours outright, which means it holds in dark mode too: a hazard label is yellow at night as well.
Two of those tokens are signal colours, accent and accent2. Nothing uses them until you ask:
<Accent><Gauge value={0.8} /></Accent>
<Accent tone="secondary"><Text variant="h1">SOLD OUT</Text></Accent>Every mark and every generator draws in currentColor, so wrapping is the whole mechanism. No colour prop was added anywhere else, and a label with no palette looks exactly as it did.
Theme
Three variables are enough. They are written into your globals.css at install time, so you edit them there, not in the package.
:root {
--vl-ink: #111111;
--vl-paper: #f4f4f1;
--vl-bg: #d9d9d6;
}Dark mode is automatic: the stylesheet reacts to the .dark class used by Tailwind and shadcn, as well as [data-theme="dark"]. If your app already switches to dark, the labels follow, with no configuration.
To force a theme on a specific area, set data-vl-theme="dark" or "light" on any ancestor.
Private or paid registry
Access control happens on the server that serves the JSON, never in the code. The consumer declares an authenticated registry and the CLI sends the headers on every request:
{
"registries": {
"@vector-labels-pro": {
"url": "https://vectorlabels.com/pro/{name}.json",
"headers": {
"Authorization": "Bearer ${VECTOR_LABELS_LICENSE_KEY}"
}
}
}
}# .env.local
VECTOR_LABELS_LICENSE_KEY=...npx shadcn@latest add @vector-labels-pro/premium-blockThe key is never written into components.json, only the name of its environment variable.
Development
Monorepo. Run commands from the repository root, not from a workspace folder.
npm and pnpm both work. The layout is declared twice: in the workspaces
field of the root package.json for npm, and in pnpm-workspace.yaml for
pnpm, which does not read that field.
npm install # or: pnpm install
npm run dev # demo site
npm run typecheck # every workspace
npm run build:lib # compiles the package to dist, types included
npm run glyphs:build # regenerates glyph-data.ts from reference/labels.html
npm run registry:check # registry guardrails
REGISTRY_URL=https://your-domain.com npm run registry:build
npm run build # registry, then package, then siteregistry:check refuses to let through: a declared but missing file, two items writing to the same place, an unresolvable dependency, a "use client" in the core, a block importing outside the public API, a block with no props or no exported props type, a prop outside the shared vocabulary, a catch-all prop, or an internal repo path leaking into shipped code.
registry.json describes the items, and points straight at the package source: one set of files feeds both channels. shadcn build compiles them into static files under apps/site/public/r/, then scripts/registry-postbuild.mjs does two rewrites: the __REGISTRY_URL__ token becomes the deployment URL, and the blocks' relative import of the core becomes the @/components/vector-labels alias the CLI lands it under.
The source keeps extensionless relative imports, which is what a bundler expects when the files are copied into a project. Node's ESM loader needs them spelled out, so scripts/fix-esm-extensions.mjs adds .js to the built output only. Both channels get the form they need, from one source.
reference/labels.html is the original sheet, in a single standalone file. It serves as the visual reference and as the source for pictogram extraction.
Layout
packages/vector-labels/ the library, publishable to npm
src/ chassis, type, marks, generators, glyphs, CSS
src/blocks/ composed labels, ready to copy
apps/site/ demo site, and host of the registry
public/r/ build output, what the shadcn CLI consumes
registry.json maps the package source into registry items
scripts/ glyph extraction, guardrails, build post-processing
reference/ original HTML sheetLicence
All rights reserved. No open source licence at this stage.
