<bmx-label-designer>
A label designer for the page: shipping labels, product and shelf labels, asset tags, name badges, sample tubes, price tickets. Lay out text, barcodes (Code 128, GS1-128, EAN, UPC, ITF, Code 39, QR, Data Matrix, PDF417), pictures and shapes on a label measured in millimetres or inches; bind them to fields; load a CSV or JSON file and print a label for every row - on A4 or Letter sheets of die-cut labels, or on a thermal printer's roll as PDF or as ZPL the printer runs natively.
11 properties · 4 events · 12 methods · 5 parts
Example
Choose an element to edit it; drag to move, pull a handle to resize. The Data tab steps through the records; Sheet preview shows every page; Print and Export make the whole job.
Show markup
<div role="group" aria-label="Sample labels" style="display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 12px">
<button type="button" class="ex-label-pick" data-sample="shipping" aria-pressed="true">Shipping label (roll)</button>
<button type="button" class="ex-label-pick" data-sample="shelf" aria-pressed="false">Shelf labels (A4 sheet)</button>
<button type="button" class="ex-label-pick" data-sample="asset" aria-pressed="false">Asset tags (300 dpi roll)</button>
</div>
<bmx-label-designer id="ex-labels" style="--bmx-label-designer-height: 44rem"></bmx-label-designer>
<p class="note" id="ex-labels-out" role="status" style="margin: 12px 0 0">Choose an element to edit it; drag to move, pull a handle to resize. The Data tab steps through the records; Sheet preview shows every page; Print and Export make the whole job.</p>
<script type="module">
await customElements.whenDefined('bmx-label-designer');
const designer = document.getElementById('ex-labels');
const out = document.getElementById('ex-labels-out');
// A small mark for the asset tags, drawn here so the example needs no picture files.
const mark =
'data:image/svg+xml;utf8,' +
encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="9" fill="#111"/><path d="M12 9v22h17" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="27" cy="13" r="4" fill="#fff"/></svg>');
const samples = {
// A 100 x 150 mm thermal shipping label: sender, service panel, address, QR, tracking barcode and an SSCC.
shipping: {
template: {
name: 'Shipping label',
unit: 'mm',
width: 100,
height: 150,
dpi: 203,
stock: 'roll-100x150',
fields: [{ name: 'name', sample: 'Ada Lovelace' }],
serial: { start: 1, step: 1, digits: 0 },
elements: [
{ id: 'from', type: 'text', x: 4, y: 4, width: 60, height: 17, text: 'FROM\nLarkfield Supply Co.\n12 Mill Lane, Bristol BS1 4XE', size: 8, fit: 'shrink' },
{ id: 'service', type: 'text', x: 68, y: 4, width: 28, height: 17, text: '{{service|upper}}', size: 16, bold: true, align: 'center', valign: 'middle', color: '#ffffff', background: '#000000' },
{ id: 'rule-1', type: 'line', x: 0, y: 23, width: 100, height: 1, orientation: 'horizontal', thickness: 1.5 },
{ id: 'to-caption', type: 'text', x: 4, y: 26, width: 30, height: 4, text: 'SHIP TO', size: 7, bold: true },
{ id: 'name', type: 'text', x: 4, y: 31, width: 92, height: 8, text: '{{name}}', size: 16, bold: true },
{ id: 'address', type: 'text', x: 4, y: 40, width: 62, height: 18, text: '{{address}}\n{{city}}', size: 12 },
{ id: 'postcode', type: 'text', x: 4, y: 59, width: 62, height: 13, text: '{{postcode|upper}}', size: 30, bold: true },
{ id: 'order', type: 'text', x: 4, y: 74, width: 62, height: 6, text: 'Order {{order}} · {{weight}} kg · parcel {{index}} of {{count}}', size: 8 },
{ id: 'qr', type: 'barcode', x: 70, y: 52, width: 26, height: 26, symbology: 'qr', value: 'https://track.example.com/{{tracking}}' },
{ id: 'rule-2', type: 'line', x: 0, y: 84, width: 100, height: 1, orientation: 'horizontal', thickness: 1.5 },
{ id: 'tracking', type: 'barcode', x: 8, y: 88, width: 84, height: 28, symbology: 'code128', value: '{{tracking}}' },
{ id: 'rule-3', type: 'line', x: 0, y: 120, width: 100, height: 1, orientation: 'horizontal', thickness: 1.5 },
{ id: 'sscc', type: 'barcode', x: 4, y: 123, width: 92, height: 24, symbology: 'gs1-128', value: '(00){{sscc|check}}' },
],
},
records: `name,address,city,postcode,service,tracking,sscc,order,weight
Ada Lovelace,4 Orchard Row,Bath,BA1 2QH,Express,LK204518860GB,35012345000004518,SO-10421,1.2
Grace Hopper,"Flat 3, 19 Harbour Street",Whitby,YO21 3PT,Standard,LK204518877GB,35012345000004519,SO-10422,0.6
Alan Turing,The Old Forge,Wilmslow,SK9 5AF,Express,LK204518891GB,35012345000004520,SO-10423,3.4
Katherine Johnson,27 Kingfisher Way,Norwich,NR3 1AU,Next day,LK204518902GB,35012345000004521,SO-10424,2.0
Mary Somerville,8 Cliff Terrace,Burntisland,KY3 9AZ,Standard,LK204518916GB,35012345000004522,SO-10425,0.9`,
},
// Shelf-edge labels on an A4 sheet of 21: product, size, price, EAN-13 and a unit price.
shelf: {
template: {
name: 'Shelf labels',
unit: 'mm',
width: 63.5,
height: 38.1,
dpi: 300,
corner: 2,
stock: 'a4-21',
elements: [
{ id: 'product', type: 'text', x: 3, y: 2.5, width: 57.5, height: 9, text: '{{product}}', size: 12, bold: true },
{ id: 'detail', type: 'text', x: 3, y: 11.5, width: 34, height: 5, text: '{{size}} · {{sku}}', size: 7, color: '#444444' },
{ id: 'price', type: 'text', x: 33, y: 16.5, width: 27.5, height: 12, text: '£{{price|number:2}}', size: 22, bold: true, align: 'right', valign: 'middle' },
{ id: 'ean', type: 'barcode', x: 3, y: 18, width: 29, height: 17.5, symbology: 'ean13', value: '{{ean}}' },
{ id: 'unit', type: 'text', x: 33, y: 29, width: 27.5, height: 5, text: '{{unit}}', size: 6.5, align: 'right', color: '#444444' },
{ id: 'offer', type: 'text', x: 41, y: 11, width: 19.5, height: 5, text: 'OFFER', size: 7, bold: true, align: 'center', valign: 'middle', color: '#ffffff', background: '#b91c1c', printIf: 'offer' },
],
},
records: `product,size,sku,price,ean,unit,offer
Stoneground wholemeal flour,1.5 kg,BK-0141,2.35,501234500141,£1.57 per kg,
Cold-pressed rapeseed oil,500 ml,OL-0207,4.80,501234500207,£9.60 per litre,yes
Wildflower honey,340 g,HN-0033,6.25,501234500033,£18.38 per kg,
Rolled jumbo oats,1 kg,CR-0518,1.95,501234500518,£1.95 per kg,yes
Sea salt flakes,250 g,SP-0090,3.10,501234500090,£12.40 per kg,
Dark roast coffee beans,1 kg,CF-0712,14.50,501234500712,£14.50 per kg,
Smoked paprika,45 g,SP-0115,2.20,501234500115,£4.89 per 100 g,
Organic red lentils,750 g,PL-0304,2.65,501234500304,£3.53 per kg,yes`,
},
// Asset tags, 57 x 32 mm at 300 dpi: a serial number that counts up, as Data Matrix and Code 128.
asset: {
template: {
name: 'Asset tags',
unit: 'mm',
width: 57,
height: 32,
dpi: 300,
corner: 2,
stock: 'roll-57x32',
serial: { start: 1040, step: 1, digits: 6 },
elements: [
{ id: 'mark', type: 'image', x: 3, y: 3, width: 9, height: 9, src: mark, alt: 'Larkfield mark' },
{ id: 'owner', type: 'text', x: 14, y: 3, width: 40, height: 4.5, text: 'LARKFIELD SUPPLY CO.', size: 9, bold: true },
{ id: 'return', type: 'text', x: 14, y: 7.5, width: 40, height: 5.5, text: 'Company property. If found,\ncall 0117 496 0000', size: 6 },
{ id: 'matrix', type: 'barcode', x: 3, y: 14.5, width: 14, height: 14, symbology: 'datamatrix', value: 'LSC-{{serial}}' },
{ id: 'bars', type: 'barcode', x: 19, y: 14.5, width: 35, height: 9, symbology: 'code128', value: 'LSC{{serial}}', showText: false },
{ id: 'number', type: 'text', x: 19, y: 24.5, width: 35, height: 4.5, text: 'LSC-{{serial}} · {{category}}', size: 7.5, bold: true, font: 'mono' },
],
},
records: `category,location
Laptop,Studio
Laptop,Studio
Monitor,Studio
Monitor,Workshop
Printer,Office
Camera,Studio
Tablet,Workshop
Router,Office
Scanner,Office
Laptop,Workshop`,
},
};
const show = name => {
designer.template = samples[name].template;
designer.records = samples[name].records;
designer.mode = 'design';
document.querySelectorAll('.ex-label-pick').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.sample === name)));
};
document.querySelectorAll('.ex-label-pick').forEach(b => b.addEventListener('click', () => show(b.dataset.sample)));
show('shipping');
designer.addEventListener('bmxChange', e => (out.textContent = `bmxChange: ${e.detail.reason}. ${e.detail.template.elements.length} elements - save e.detail.template to keep the design.`));
designer.addEventListener('bmxRecordChange', e => (out.textContent = `bmxRecordChange: record ${e.detail.index + 1}.`));
designer.addEventListener('bmxExport', e => {
// Cancel to keep the file from downloading - to send ZPL straight to a printer, for instance.
out.textContent = `bmxExport: ${e.detail.filename} (${typeof e.detail.data === 'string' ? `${e.detail.data.length} characters` : `${e.detail.data.size} bytes`}).`;
});
</script>
<bmx-label-designer id="labels"></bmx-label-designer>
<script>
labels.template = { unit: 'mm', width: 100, height: 50, elements: [
{ type: 'text', x: 4, y: 4, width: 92, height: 10, text: '{{name}}', size: 16, bold: true },
{ type: 'barcode', x: 4, y: 18, width: 70, height: 26, symbology: 'code128', value: '{{sku}}' },
] };
labels.records = 'name,sku\nBlue mug,MUG-001\nRed mug,MUG-002';
</script>
Every output comes from one layout (label-layout.ts), so the design, the
sheet preview, the printout, the PDF and the ZPL agree. Barcodes are sized
to whole printer dots at the template's resolution, so what is designed
is what scans.
Accessibility: the design surface is focusable; Page Down and Page Up step through the elements, the arrow keys move the chosen one (Control resizes) and what it is and where it sits is announced. The Layers tab is a listbox of every element, the Properties tab a form of labelled fields, and every tool is a labelled button.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
copies |
copies |
number |
1 |
Copies of each record. Mutable. |
gridSize |
grid-size |
number |
— | The grid, in the template's unit. Default 1 mm, 0.05 in or 4 pt. |
label |
label |
string |
— | The designer's accessible name. Default: the template's name. |
mode |
mode |
'design' | 'preview' |
'design' |
design to edit, preview to see the sheets as they will print. Mutable. |
readonly |
readonly |
boolean |
false |
Look and print only: no editing. |
records |
records |
readonly BmxLabelRecord[] | string |
— | The data to print: an array of records, or CSV (or JSON) text. |
skip |
skip |
number |
0 |
Labels already used on the first sheet, so a part-used sheet can be finished. Mutable. |
snap |
snap |
boolean |
true |
Snap to the grid and to other elements while dragging. Mutable. |
stocks |
property only | readonly Record<string, unknown>[] |
— | Label stocks of your own, besides the built-in sheets and rolls. |
strings |
strings |
Partial<BmxLabelDesignerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
template |
template |
BmxLabelTemplate | Record<string, unknown> | string |
— | The template: an object or JSON. Mutable: it is replaced as the label is edited. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxLabelChangeDetail |
Fired when the template is edited, with the whole of it. |
bmxExport |
BmxLabelExportDetail |
Fired when an export is made from the Export menu. Cancel it to stop the download and use data yourself. |
bmxRecordChange |
BmxLabelRecordDetail |
Fired when another record is shown. |
bmxSelectionChange |
BmxLabelSelectionDetail |
Fired when the selected elements change. |
Methods
| Method | Signature | Description |
|---|---|---|
addElement |
addElement(type: BmxLabelElementType, properties?: Record<string, unknown>) => Promise<string> |
Adds an element (text, barcode, image, box, ellipse, line) with any of its properties; returns its id. |
exportPdf |
exportPdf(options?: BmxLabelExportOptions) => Promise<Blob> |
The job as a PDF: on the template's sheets or roll, or one label per page. |
exportPng |
exportPng() => Promise<Blob> |
The label showing as a PNG at the printer's resolution. |
exportSvg |
exportSvg() => Promise<string> |
The label showing as SVG, pictures included. |
exportZpl |
exportZpl(options?: BmxLabelExportOptions) => Promise<string> |
The job as ZPL for a thermal printer: one format per label, pictures as black and white dots. |
getTemplate |
getTemplate() => Promise<BmxLabelTemplate> |
The template as it stands. |
loadData |
loadData(text: string) => Promise<number> |
Loads CSV or JSON text as the data; returns how many records it has. |
print |
print(options?: BmxLabelExportOptions) => Promise<void> |
Prints the job from the browser: the pages exactly as the sheet preview shows them, at their real size. |
redo |
redo() => Promise<void> |
|
selectElements |
selectElements(ids: string[]) => Promise<void> |
Selects elements by id. |
showRecord |
showRecord(index: number) => Promise<void> |
Shows a record (0-based). |
undo |
undo() => Promise<void> |
CSS shadow parts
| Part | Description |
|---|---|
canvas |
The scrolling area the label sits in. |
label |
The label itself. |
panel |
The Properties, Layers and Data tabs. |
status |
The status line along the bottom. |
toolbar |
The tools along the top. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-label-designer-accent |
Selection, handles and guides. |
--bmx-label-designer-desk |
The background around the label. |
--bmx-label-designer-height |
The designer's height (on a wide screen; narrow, it grows to fit). |
--bmx-label-designer-panel-width |
The width of the side panel. |
--bmx-label-designer-radius |
Corner radius of the designer. |