Escape

Shipping label template for JavaScript

A shipping label is the one document a parcel can’t travel without. It’s 4 by 6 inches, the size a thermal label printer takes. The address has to read from across a sorting hall, and a scanner has to read the barcode under it. A shipment of three parcels needs three labels, each saying which of the three it is. This is a ready-made quario shipping label — copy it, point it at your shipment, and render one label per parcel to PDF or HTML.

The template

The whole label is a definition — data you can version and review. The sender block, the service band, the delivery address, the parcel line and the barcode are all in here. This is the exact file the preview below renders:

shipping-label.report.json
{
  "data": "$.shipment.parcels[*]",
  "sort": [{ "by": "[email protected]" }],
  "aggregates": { "parcels": "count" },
  "style": { "family": "sans", "size": 9 },
  "groups": [{ "name": "parcel", "by": "[email protected]", "break": "between" }],
  "detail": [
    {
      "type": "split",
      "style": { "paddingBottom": 8, "borderBottomWidth": 1.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" },
      "slots": [
        { "type": "text", "value": [{ "value": "From\n", "style": { "family": "mono", "size": 6, "uppercase": true, "color": "#989a97" } }, { "value": "{{ $.input.shipment.from.name }}\n{{ $.input.shipment.from.street }}\n{{ $.input.shipment.from.city }}\n{{ $.input.shipment.from.country }}" }], "width": 62,
          "style": { "size": 7, "color": "#5e6062" } },
        { "type": "text", "value": "{{ $.input.shipment.service }}",
          "style": { "size": 15, "bold": true, "uppercase": true, "align": "center", "valign": "middle",
            "color": "#ffffff", "background": "#1e1f22", "paddingTop": 6, "paddingBottom": 6 } }
      ]
    },
    { "type": "text", "value": "Ship to",
      "style": { "family": "mono", "size": 6, "uppercase": true, "color": "#989a97", "spaceBefore": 12 } },
    { "type": "text", "value": "{{ $.input.shipment.to.name }}",
      "style": { "size": 16, "bold": true, "color": "#1e1f22", "spaceBefore": 2 } },
    { "type": "text", "value": "Attn. {{ $.input.shipment.to.attn }}",
      "visible": "=$.input.shipment.to.attn != null",
      "style": { "size": 10, "color": "#5e6062" } },
    { "type": "text", "value": "{{ $.input.shipment.to.street }}\n{{ $.input.shipment.to.city }}",
      "style": { "size": 12, "color": "#1e1f22", "spaceBefore": 4 } },
    { "type": "text", "value": "{{ $.input.shipment.to.country }}",
      "style": { "size": 12, "bold": true, "uppercase": true, "color": "#1e1f22" } },
    { "type": "text", "value": "Tel. {{ $.input.shipment.to.phone }}",
      "visible": "=$.input.shipment.to.phone != null",
      "style": { "size": 8, "color": "#5e6062", "spaceBefore": 4, "spaceAfter": 12 } },
    {
      "type": "grid",
      "columns": [
        { "width": 30, "style": { "paddingTop": 6 } },
        { "width": 30, "style": { "paddingTop": 6 } },
        { "style": { "paddingTop": 6, "align": "right" } }
      ],
      "rows": [
        {
          "style": { "family": "mono", "size": 6, "uppercase": true, "color": "#989a97",
            "borderTopWidth": 1.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "slots": [
            { "type": "text", "value": "Parcel" },
            { "type": "text", "value": "Weight" },
            { "type": "text", "value": "Shipped" }
          ]
        },
        {
          "style": { "size": 13, "bold": true, "color": "#1e1f22", "paddingBottom": 8 },
          "slots": [
            { "type": "text", "value": "{{ @.number }} of {{ $.parcels }}", "style": { "paddingTop": 0 } },
            { "type": "text", "value": "{{ @.kg }} kg", "style": { "paddingTop": 0 } },
            { "type": "text", "value": "{{ $.input.shipment.shipped }}", "style": { "paddingTop": 0, "format": "date" } }
          ]
        }
      ]
    },
    { "type": "image", "source": "=barcode(@.tracking)", "fit": "width", "alt": "Barcode for tracking number {{ @.tracking }}",
      "style": { "spaceBefore": 4, "paddingTop": 10, "borderTopWidth": 1.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" } },
    { "type": "text", "value": "{{ @.tracking }}",
      "style": { "family": "mono", "size": 10, "color": "#1e1f22", "align": "center", "spaceBefore": 4 } },
    { "type": "text", "value": "{{ $.input.shipment.carrier }} · Order {{ $.input.shipment.orderRef }}",
      "style": { "size": 7, "color": "#989a97", "align": "center", "spaceBefore": 8, "spaceAfter": 40 } }
  ]
}

The rows are the parcels, $.shipment.parcels[*], and the shipment itself is $.input. One label per parcel is a group on the parcel number with "break": "between". Every boundary between two parcels turns a page, and the first label starts on the first page. The aggregate $.parcels counts the rows, so each label reads 1 of 3, 2 of 3 and 3 of 3 without the data saying how many there are.

detail is an array of items rather than a table, because a label isn’t a grid and a table cell can’t hold an image. The barcode is an image item whose source is =barcode(@.tracking). That row’s tracking number goes in, and the bars come back as SVG bytes. "fit": "width" scales them to the label’s full width, which is what a scanner wants.

The service band is one text slot with a dark background and white type. Each rule between the blocks is a border on a split, a grid row, or the barcode itself. Nothing on the label needs a coordinate.

The barcode

A Code 128 barcode, the symbology carriers print for a tracking number. bwip-js draws it as SVG, synchronously, and it runs in Node and in the browser. A barcode library that needs await would have to draw before the render and pass the bytes in, because an image source can’t wait:

barcode.ts
import bwipjs from "bwip-js";

/** A Code 128 barcode of `text`, returned as the SVG's bytes. */
export function barcode(text: string): Uint8Array {
  const svg = bwipjs.toSVG({ bcid: "code128", text, height: 20 });
  // bwip-js sizes the drawing by `viewBox` alone; the image item needs a width and height to scale from.
  const [, , width, height] = svg.match(/viewBox="([^"]+)"/)![1].split(" ");
  return new TextEncoder().encode(
    svg.replace("<svg ", `<svg width="${width}" height="${height}" `),
  );
}

The data

A definition renders nothing on its own — you hand it a document. This is the sample the preview below uses. Each parcel carries its own number, tracking number and weight, and everything the labels share sits once on the shipment:

shipping-label.data.json
{
  "shipment": {
    "orderRef": "SO 2026-1042",
    "shipped": "2026-08-27",
    "service": "Express",
    "carrier": "PostNL Cargo",
    "from": {
      "name": "Quire Press Ltd.",
      "street": "Binnenkant 42",
      "city": "1011 BM Amsterdam",
      "country": "Netherlands"
    },
    "to": {
      "name": "Folio & Sons",
      "attn": "Goods inwards",
      "street": "12 Marlborough Street",
      "city": "Dublin 1, D01 T2X0",
      "country": "Ireland",
      "phone": "+353 1 555 0142"
    },
    "parcels": [
      { "number": 1, "tracking": "3SQPX4471008219", "kg": 14.2 },
      { "number": 2, "tracking": "3SQPX4471008220", "kg": 14.2 },
      { "number": 3, "tracking": "3SQPX4471008221", "kg": 13.2 }
    ]
  }
}

What it renders

This isn’t a screenshot. quario compiles the definition and data above, runs barcode.ts in your browser for each parcel, and @quario/viewer paints three labels right here. The PDF button hands you the same labels as real @quario/pdf bytes, three pages of 288 × 432 points. The bars are vector paths, which a label printer prints sharp. There’s no Word button: @quario/docx refuses an SVG image.

Rendering live with @quario/viewer4 × 6 in label

Use it

shell
npm install quario @quario/pdf bwip-js

The label’s size is a render option, not part of the document. size takes a [width, height] in points, and 4 × 6 inches is 288 × 432:

shipping-label.ts
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import label from './shipping-label.report.json' with { type: 'json' }

// `barcode` is the function from barcode.ts
const report = quario({ locale: 'en-IE' }).report(label, { barcode })

const data = await getShipment(id)                   // your warehouse data
const bytes = await report.render(pdf({ page: { size: [288, 432], margin: 14 } }), data)

writeFileSync('labels.pdf', bytes)                   // one page per parcel

The shipped date presents through the instance’s locale, 27 Aug 2026 under en-IE. Nothing on the label is money, so the instance needs no currency.

Make it yours

Because the label is declarative, changing it means changing data. A label on A6 paper is a different size in the render call, and the definition doesn’t move. A return label is the sender and the recipient swapped in the data. A routing code above the barcode is one more text item in a large mono face. A QR code in place of the bars is a different bcid in the function, as on the event ticket. It renders without eval, so an address typed into your checkout never becomes code. Security →

FAQ

Will a carrier accept this label?

The layout is yours to match what your carrier asks for. Carriers that accept your own labels usually want the tracking number as a Code 128 barcode. They also want a clear delivery address and the parcel’s weight. Check the carrier’s label specification for the minimum height and quiet zone of the bars, and set height in barcode.ts to suit.

Can I print to a thermal label printer?

Yes. Render the PDF at 288 × 432 points, the 4 × 6 inch stock most desktop thermal printers take, and print it at actual size. The bars are vector paths, so the printer draws them at its own resolution rather than scaling a picture.

Can I generate the labels as a PDF?

Yes, and it’s the format a print queue takes. @quario/pdf renders this definition straight to bytes with no browser anywhere, so a dispatch job can print the labels as the shipment closes. Full example →

Can I use this shipping label template commercially?

The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Labelling real parcels is past evaluation, so that needs a license for each production deployment. Its users, agents, and renders are never counted. Pricing →

Where are the other templates?

Browse the template gallery → — packing slips, bills of lading, delivery notes, and more.