SDK
@labelviewer/elements is one JavaScript module with both browser components: <zpl-viewer> (the
Viewer) and <zpl-editor> (the Editor). Your install serves it at /sdk/, so
there is nothing to install: load the module from the install and the fonts and the API come along automatically. This
page covers loading it, for both components.
On this install: @labelviewer/elements 1.0.1
The SDK on this server is a different build from the one this page was generated for. Rebuild the portal (npm run build:web).
Copy this into your page. The URL and the hash are this server's; the module defines <zpl-viewer> and <zpl-editor>.
<script type="module"
src="https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js"
integrity="sha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV"
crossorigin="anonymous"></script>
https://labelviewer.example.internal/sdk/1.0.1/recommendedImmutable, cached for a year, pairs withintegrity.https://labelviewer.example.internal/sdk/latest/convenienceAlways the installed version, revalidated on every load; no hash possible.
Versioned or latest#
| URL | Caching | Use it when |
|---|---|---|
/sdk/<version>/labelviewer-elements.min.js | immutable, one year | Production. Pin the version and add integrity. After a server upgrade you update the URL (old versions are not kept). |
/sdk/latest/labelviewer-elements.min.js | revalidated every time | Development, or when you want every server upgrade to reach your app without a change. No integrity possible. |
labelviewer-elements.js (readable) sits next to the minified file. Every /sdk/ file is served with
Access-Control-Allow-Origin: *, so your app can load it from any origin. Keep the URL in one place in your
configuration: after an upgrade the old versioned URL answers 404 (Install › Upgrades).
An SDK loaded from the install
- fetches its fonts from the same place (
/sdk/<version>/fonts/), - uses that install for the Editor's Test with API and API tab (
apiBasedefaults to the install's origin), and - asks that install for its license (
GET /v1/info, once per page every 10 minutes): a component whose product is not licensed shows "This label component is not licensed for this installation. Contact your administrator." instead of its content, and on a trial license the viewer's labels are watermarked and the editor's canvas shows an X (License). If the install cannot be reached the components keep working and log one console warning.
Load it per framework#
The module defines both tags once per page. Code that uses them can wait with
await customElements.whenDefined('zpl-viewer') (or 'zpl-editor').
Plain HTML#
<script type="module" src="https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js"
integrity="sha384-<hash from this page>" crossorigin="anonymous"></script>
<zpl-viewer zoom="fit"></zpl-viewer>
Dynamic import#
To load the module only where it is used (for example only on the template designer route), import the URL. Bundlers
must be told to leave it to the browser: /* @vite-ignore */ for Vite, /* webpackIgnore: true */ for webpack and the
Angular CLI.
const LABELVIEWER_SDK = 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js';
let sdk;
export const loadLabelviewer = () => (sdk ??= import(/* @vite-ignore */ /* webpackIgnore: true */ LABELVIEWER_SDK));
A dynamic import() cannot carry an integrity hash; use the script tag where you need Subresource Integrity.
Angular#
Either add the script tag to src/index.html, or load the module on demand from a service:
export const LABELVIEWER_SDK = 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js';
@Injectable({ providedIn: 'root' })
export class LabelviewerService {
private loading?: Promise<unknown>;
load() {
return (this.loading ??= import(/* webpackIgnore: true */ /* @vite-ignore */ LABELVIEWER_SDK));
}
}
- Components that use the tags need
schemas: [CUSTOM_ELEMENTS_SCHEMA].[prop]binds DOM properties,(event)listens to the element's events;$eventis typedEventin templates, so cast it to readdetail. - Zones. With zone.js, the element's own work (pointer moves while dragging, timers, font downloads) triggers change
detection, which is harmless but wasteful. Prefer zoneless change detection, or
provideZoneChangeDetection({ eventCoalescing: true }). - Under a strict
style-src, turn off critical CSS inlining ("optimization": { "styles": { "inlineCritical": false } }); that is about Angular's own CSS, not the elements.
React#
Load the module in an effect with the loadLabelviewer() above (or put the script tag in index.html).
- React 19 sets custom element properties and adds listeners for
on<event>props, so once the module is loaded the elements work as JSX. Write the event props in lower case (onchange,onsave,ondirty-change,onrender): the part afteronis the event name.onChange(camel case) is React's own synthetic event and never fires for custom elements. - React 18 only sets string attributes on custom elements and attaches no listeners to them. Pass objects and
functions through a ref and listen with
addEventListener(the Editor's React recipe does). - Next.js: the elements need the browser (
HTMLElement,customElements). Render the component on the client only,dynamic(() => import('./LabelDesigner'), { ssr: false })(from a client component in the App Router).
Vue#
Load the module in onMounted (or put the script tag in index.html), and tell the compiler that the tags are custom
elements:
// vite.config.ts
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('zpl-') } } })
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const ready = ref(false);
onMounted(async () => {
await import(/* @vite-ignore */ 'https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements.min.js');
ready.value = true;
});
</script>
<template>
<zpl-viewer v-if="ready" :zpl="template" :data="{ name: 'Ada' }" zoom="fit" />
</template>
Bind object and function props in camelCase (:data, :sampleData, :saveHandler) so Vue sets them as properties.
TypeScript types without npm#
The declarations are served next to the module: index.d.ts, react.d.ts (React 19 JSX) and vue.d.ts (Vue
templates). Download them into your project and map the package name with paths:
mkdir -p src/types/labelviewer && cd src/types/labelviewer
for f in index.d.ts react.d.ts vue.d.ts; do curl -fsSO https://labelviewer.example.internal/sdk/1.0.1/$f; done
{ "compilerOptions": { "paths": { "@labelviewer/elements": ["./src/types/labelviewer/index.d.ts"] } } }
import type { ZplEditorElement, ZplSaveDetail, ZplViewerElement } from '@labelviewer/elements';
import type {} from './types/labelviewer/react'; // JSX typings for <zpl-editor> / <zpl-viewer> (React 19); Vue: './types/labelviewer/vue'
import type imports disappear from the build, so nothing tries to bundle the package. index.d.ts also adds both tags
to HTMLElementTagNameMap, so document.querySelector('zpl-viewer') is typed. Download the files again when you change
the SDK version.
npm (optional)#
If you prefer a dependency in package.json, the install serves the package tarball too:
npm install https://labelviewer.example.internal/sdk/1.0.1/labelviewer-elements-1.0.1.tgz
import '@labelviewer/elements'; // defines <zpl-editor> and <zpl-viewer>
A bundled copy does not know where your install is. Point it there with fonts-base-url (or copy
node_modules/@labelviewer/elements/dist/fonts/ to your static files) and api-base (the install whose license
applies, and the Editor's API):
<zpl-viewer fonts-base-url="https://labelviewer.example.internal/sdk/1.0.1/fonts/" api-base="https://labelviewer.example.internal"></zpl-viewer>
<zpl-editor fonts-base-url="https://labelviewer.example.internal/sdk/1.0.1/fonts/" api-base="https://labelviewer.example.internal"></zpl-editor>
Fonts#
The renderer needs three TrueType files (RobotoCondensed-Bold.ttf, DejaVuSansMono.ttf, DejaVuSansMono-Bold.ttf,
844 KiB, about 490 KiB gzipped). They are fetched once per page and shared by every element.
- Loaded from the install (script tag or
import()of the/sdk/URL): nothing to do; they load fromfonts/next to the script. - Bundled from npm: set
fonts-base-url, see above. - Your own bytes:
registerFonts({ 'RobotoCondensed-Bold.ttf': bytes, ... })(ArrayBuffer or Uint8Array, every name inFONT_FILE_NAMES) before the first element connects. Nothing is fetched then.
The first element that connects decides the URL. loadFonts(url) starts the download early.
Content Security Policy#
The elements need no 'unsafe-inline' and no 'unsafe-eval' (the package build fails if the bundle contains eval or
new Function). Styles are constructable stylesheets and CSSOM properties, which style-src does not restrict. For an
app that loads the SDK from its install:
script-src 'self' https://labelviewer.example.internal;
connect-src 'self' https://labelviewer.example.internal;
img-src 'self' blob:;
script-src: the module itself.connect-src: the font files are downloaded withfetch, so their origin goes here (not infont-src), and so does the API for the Editor's Test with API.img-src blob::<zpl-viewer>shows the label as ablob:image, andprint()uses one too.- Trusted Types (
require-trusted-types-for 'script'): allow the policy namelabelviewer(trusted-types labelviewer). The elements use it only for their own static templates.
Browser support and size#
Current Chrome, Edge, Firefox and Safari: Chrome/Edge 111+, Firefox 113+, Safari 16.4+ (constructable stylesheets,
container queries, color-mix(), ES2022). There are no polyfills. The tests run in Chrome.
| File | Raw | gzip |
|---|---|---|
labelviewer-elements.min.js | 1.43 MiB | 422 KiB |
| fonts (3 files) | 844 KiB | ~490 KiB |
About two thirds of the script is the barcode library (bwip-js, used for Data Matrix, Aztec, MaxiCode, DataBar and
other symbologies), then the font parser (~240 KiB), the renderer (~140 KiB) and the designer UI (~110 KiB). Versioned
/sdk/ URLs are cached by browsers for a year. To keep the script off your first page load, load it with import()
where it is used.
Security#
- The elements render in the browser and store nothing by default. Only the Editor's
autosave-keywrites tolocalStorageon your origin. - Their only server calls are the font downloads, the license check (
GET ${apiBase}/v1/info) and the Editor's Test with API (POST ${apiBase}/v1/merge), sent withcredentials: 'omit'and no credentials of any kind. - The labelviewer API has no authentication, by design: keep the install on your internal network (Install › Access control).
Troubleshooting#
| Symptom | Cause |
|---|---|
| Text on the label is missing, or boxes only | The fonts did not load: check the network tab for fonts/...ttf errors. Bundled from npm? Set fonts-base-url. CSP? Fonts need connect-src. An error event with source: 'fonts' says so too. |
| The module does not load (CORS or 404) | Check the URL above: after a server upgrade old /sdk/<version>/ URLs answer 404. /sdk/ files always carry Access-Control-Allow-Origin: *; a proxy in between must not strip it. |
integrity mismatch | The hash belongs to another version or build. Copy URL and hash together from this page. |
| "This label component is not licensed for this installation" | The install's license does not cover that product, has expired or is not valid: see its Status page and License. The element also fires error with source: 'license'. |
| Console: "cannot check the license of this installation" | The component could not reach ${apiBase}/v1/info (bundled without api-base, network, CSP connect-src). It keeps working. |
The viewer shows nothing, and the console reports img-src | Add blob: to img-src. |
| Angular: "zpl-editor is not a known element" | Add CUSTOM_ELEMENTS_SCHEMA to the component's schemas. |
| Vue: "Failed to resolve component: zpl-viewer" | Set compilerOptions.isCustomElement. |
| The bundler fails on the SDK URL | Add /* @vite-ignore */ (Vite) or /* webpackIgnore: true */ (webpack, Angular CLI) inside the import(). |
HTMLElement is not defined during SSR | Load the SDK only in the browser (see React). |
Objects show up as [object Object] attributes | React 18, or the module was loaded after the framework rendered the element. Set them as properties (a ref), or wait for customElements.whenDefined(...). |
| CSP errors | See Content Security Policy. |
Files on this install#
Every file the install serves under /sdk/<version>/, with its size and SRI hash (the portal shows them for this install).
| File | Size | SRI hash (sha384) |
|---|---|---|
fonts/DejaVuSansMono-Bold.ttf | 324 KiB | sha384-/HuWuQ8I5VcyDsTF+p5szVGtEA7khIUX8/tvN/J1Mifnzz/rkNU1zpG+GfnBwzyt |
fonts/DejaVuSansMono.ttf | 333 KiB | sha384-hH2n2s6XHqm1MrgAnRVYFnFUMMCbuKmyQRLhMOBYwnnouvzw0ezopjj83uhQNiM/ |
fonts/LICENSE-Adafruit-GFX.txt | 1 KiB | sha384-qDKhzJTpVzuJ/vWlqMzwTm1dLD8DHCAw7xCe4/7w0ohbBDdzKDfP3blPx17AE87X |
fonts/LICENSE-DejaVu.txt | 9 KiB | sha384-grgle1y36XevlzjmAQ76+4tlxta1TkKWZ2GjVsyDXIcbdQIPjTxkl0eFDvB9Qe06 |
fonts/LICENSE-RobotoCondensed.txt | 11 KiB | sha384-II9e1ieUDl5AxyiVq3/FflTua1Sr0kMJ25e6imG7rXg7SiAsA2VemsvEqVsLqM7/ |
fonts/RobotoCondensed-Bold.ttf | 166 KiB | sha384-Q4NcziKMMWOqCt6K8cRF9LDPZd/70gWy33Be43cm5Io5IhToryVI6FiWbcJuvMpC |
index.d.ts | 12 KiB | sha384-67lwZ5tqQU0pPO0XvG24I+hMp1q8zPXwfMvGQS6cMIjP3difpZG5kUiI+Vl8qrwZ |
labelviewer-elements-1.0.1.tgz | 1.47 MiB | sha384-IbB4fadcJvErkhS35OW80V6K+5txsErPgvDA62WtVK1rUynVdNBSAiAc6iQ9B5Ea |
labelviewer-elements.js | 1.64 MiB | sha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV |
labelviewer-elements.min.js | 1.64 MiB | sha384-UiBvh+sXcRxua68FPRo4L0qmnW3QoOGISuBtFtqldU8koeeZIyyBenyqkujDz7gV |
react.d.ts | 3 KiB | sha384-pk4E2NIyPkTgeTfuaRREk08/QwJaNkLK/OaGo22DdkQ2+MIF8a6K44yPaGF2eRBp |
vue.d.ts | 2 KiB | sha384-wkT/XoS3oTRkrKWjSakdMr1E5oIp24ZptuPsGfnTLipnaSfhbBz3ng39IS3PAJUL |
Every file is also listed in manifest.json.