Installation & Integration Guide

Install ScanBridge and add scanning to your app.

ScanBridge ships as two tarballs hosted on our server: @scanner/core (the engine) and @scanner/web (the widget). This guide walks you through installing the packages and dropping the scanner widget into your application.

Install the packages

@scanner/web depends on @scanner/core, so install core first, then web, or add both at once.

Do not run npm install @scanner/core on its own. These packages are not published to the public npm registry and must be installed from the tarball URLs below.

Option A, quick install in one command

Install both in a single command, core first:

bash
npm install \
  https://demo.scanbridge.dev/install-files/scanner-core-0.2.0.tgz \
  https://demo.scanbridge.dev/install-files/scanner-web-0.2.0.tgz

On Windows PowerShell, put it on one line (no backslashes):

powershell
npm install https://demo.scanbridge.dev/install-files/scanner-core-0.2.0.tgz https://demo.scanbridge.dev/install-files/scanner-web-0.2.0.tgz

Option B, pin it in package.json, recommended for CI

Add both to your dependencies, then run npm install. This is reproducible and survives a clean checkout:

package.json
{
  "dependencies": {
    "@scanner/core": "https://demo.scanbridge.dev/install-files/scanner-core-0.2.0.tgz",
    "@scanner/web": "https://demo.scanbridge.dev/install-files/scanner-web-0.2.0.tgz"
  }
}
bash
npm install

Updating to a new version: bump the version number in the URL (e.g. 0.2.0 → 0.2.1), delete node_modules/@scanner (or run npm install --force), and reinstall so the new tarball is fetched instead of a cached copy.

Add to your project

Import the package once, then render the <scanner-widget> custom element. Pick your framework below.

You need a license key. Set the license-key attribute on the widget and replace SB-XXXX-XXXX-XXXX with the key issued to your organisation. Without a valid key the widget will not open the scanning flow and shows a license message instead.

  1. Install the package.
  2. Import it once, for example in main.ts.
  3. Allow the custom element with CUSTOM_ELEMENTS_SCHEMA.
main.ts
import '@scanner/web';
app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <scanner-widget license-key="SB-XXXX-XXXX-XXXX"></scanner-widget>
  `,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {}
  1. Install the package.
  2. Import it once in your app entry or component.
  3. Render the custom element directly.
App.tsx
import '@scanner/web';

export function App() {
  return <scanner-widget license-key="SB-XXXX-XXXX-XXXX" />;
}

TypeScript (React / Next.js)

If TypeScript complains that scanner-widget is not a known JSX element, add a one-time declaration (e.g. in scanner-widget.d.ts):

scanner-widget.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from 'react';

declare global {
  namespace JSX {
    interface IntrinsicElements {
      'scanner-widget': DetailedHTMLProps<
        HTMLAttributes<HTMLElement> & {
          'license-key'?: string;
          'file-type'?: 'pdf' | 'png' | 'jpg';
          lang?: string;
          open?: boolean;
        },
        HTMLElement
      >;
    }
  }
}

Use it in a client component.

page.tsx
'use client';

import '@scanner/web';

export default function Page() {
  return <scanner-widget license-key="SB-XXXX-XXXX-XXXX" />;
}

See the React tab for the one-time TypeScript declaration if your editor flags scanner-widget as an unknown element.

  1. Install the package.
  2. Import it once, for example in main.ts.
  3. Make sure Vue treats it as a custom element if needed in your setup.
main.ts
import '@scanner/web';
App.vue
<template>
  <scanner-widget license-key="SB-XXXX-XXXX-XXXX" />
</template>
+page.svelte
<script lang="ts">
  import '@scanner/web';
</script>

<scanner-widget license-key="SB-XXXX-XXXX-XXXX" />
index.html
<scanner-widget license-key="SB-XXXX-XXXX-XXXX"></scanner-widget>
<script type="module">
  import '@scanner/web';
</script>

Common options

These attributes work the same way in every framework.

file-type string

Controls the output file type.

pdf (default) png jpg
html
<scanner-widget file-type="pdf"></scanner-widget>
theme string

Chooses between the light and dark appearance.

light (default) dark auto

Light is the default. The widget is a guest inside your application, and your application's theme rarely tracks the end user's Windows setting. If the widget followed Windows on its own, a light host would get a dark dialog the moment somebody flipped a setting you know nothing about. You decide instead, and every machine looks the same.

Use auto only if your own interface already follows Windows.

html
<scanner-widget theme="dark"></scanner-widget>

In Angular there is nothing extra to install. The widget is a custom element, so bind the attribute the same way you bind any other, and drive it from whatever already holds your own theme.

ts
<scanner-widget [attr.theme]="isDark ? 'dark' : 'light'"></scanner-widget>

Use [attr.theme] and not [theme]. A custom element has no Angular input of that name, so the property form is silently ignored and the widget stays light.

If your application lets the user switch themes while it runs, set the attribute from the same place that switches the rest of your interface.

js
document.querySelector('scanner-widget')
        .setAttribute('theme', isDark ? 'dark' : 'light');

The widget defines every colour it uses on its own element, so your page cannot recolour it by accident. Both themes are checked for contrast, and no text falls below the 4.5:1 threshold in either.

lang string

Controls the widget language.

en (default) ar es fr de ru it pt-BR

Language selection is part of your licence. If your licence does not include it, the widget stays in English whatever you set here, and no error is raised. Ask us if you are not sure what your key covers.

html
<scanner-widget lang="en"></scanner-widget>
open boolean

Controls whether the widget modal is visible. Default: true.

html
<scanner-widget open></scanner-widget>
license-key string

The ScanBridge license key issued to your organisation. The integrator sets this attribute once; the widget transparently activates the local helper service on each end-user machine the first time the page is opened. End users never have to type or paste the key.

html
<scanner-widget license-key="SB-XXXX-XXXX-XXXX"></scanner-widget>

Angular example using an environment variable so the key is not hard-coded into the template:

scan.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { environment } from 'environments/environment';
import '@scanner/web';

@Component({
  selector: 'app-scan',
  standalone: true,
  template: `
    <scanner-widget
      [attr.license-key]="licenseKey"
      [attr.theme]="theme"
      file-type="pdf">
    </scanner-widget>
  `,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ScanComponent {
  licenseKey = environment.scanBridgeLicenseKey;
  theme: 'light' | 'dark' = 'light';
}

Why the key in the JS bundle is safe: the licensing server binds each license to a list of allowed origins. A key copied from your production bundle and used on a different domain is rejected by the licensing API.

Appearance

What the widget looks like inside your application, and what it will not do to your page.

It cannot be restyled by accident

The widget renders in a shadow root and defines every colour it uses on its own element. Your stylesheet cannot reach inside it, and it does not read anything from your page. Drop it into a design system with its own resets and it looks the same as it does here.

Light and dark

Light is the default. Switch with theme, described under Options. Both themes are checked for contrast: body text lands at 12.63:1 on light and 13.62:1 on dark, and no text falls below the 4.5:1 threshold in either.

Dropdowns

The lists that <select> opens are styled to match the rest of the widget, using appearance: base-select. That works in Chrome and Edge 135+ and in Safari 27. Anywhere else, including Firefox, the browser draws its own native dropdown instead.

Nothing breaks either way. The styled version is wrapped in @supports, so a browser without it simply keeps the dropdown it has always drawn.

Flat by design

There are no drop shadows anywhere. Separation is done with borders, which keeps the widget from looking like a floating card in applications that are themselves flat.

Fonts

No web font is downloaded. The interface asks for Inter and numeric fields ask for a monospace face, both falling back to whatever the machine already has. The widget will not add a network request or a layout shift to your page.

License

The widget activates and validates the license against the local helper service automatically, so your end users never type or paste a key. You only set the license-key attribute once.

What to expect:

  • Valid license: the widget opens the scanning flow normally.
  • No / expired / revoked / not-allowed license: the widget does not open the scanning flow (fail closed) and shows a short license message instead. This is intentional and cannot be bypassed by the page.
  • Wrong domain: a key used on an origin that is not on the license's allow-list is rejected. Make sure your production domain is registered for the license.
  • Offline: once a machine has a valid license, the widget keeps working offline until the license actually expires. A revoked or expired verdict from the server always wins over a cached one.

Activation is stored per web-application host in the current Windows user profile and happens transparently on first use. Different sites can use their own keys with the same ScanBridge installation, and non-standard ports are covered by the same licensed host. Renewals do not require reinstalling the helper.

How to use

  1. Render <scanner-widget>.
  2. Optionally set file-type, lang, or open.
  3. Listen for saved and closed, or use onSaved and onClosed.

Example with addEventListener

html
<scanner-widget id="scanner" file-type="pdf"></scanner-widget>
<script type="module">
  import '@scanner/web';

  const scanner = document.getElementById('scanner');

  scanner.addEventListener('saved', (event) => {
    console.log(event.detail);
  });

  scanner.addEventListener('closed', () => {
    console.log('Scanner closed');
  });
</script>

Example with callbacks

html
<scanner-widget id="scanner" file-type="pdf"></scanner-widget>
<script type="module">
  import '@scanner/web';

  const scanner = document.getElementById('scanner');

  scanner.onSaved = (detail) => {
    console.log(detail);
  };

  scanner.onClosed = () => {
    console.log('Scanner closed');
  };
</script>

Outputs & events

The widget emits these custom events.

savedCustomEvent

Emitted after save finishes successfully.

type
CustomEvent<
  | { fileType: 'pdf'; blob: Blob }
  | {
      fileType: 'png' | 'jpg';
      pages: Array<{
        id: string;
        name: string;
        url: string;
        mimeType: string;
        blob: Blob;
      }>;
    }
>
example
scanner.addEventListener('saved', (event) => {
  const detail = event.detail;

  if (detail.fileType === 'pdf') {
    console.log(detail.blob);
    return;
  }

  console.log(detail.pages);
});
closedCustomEvent<void>

Emitted when the widget is closed.

example
scanner.addEventListener('closed', () => {
  console.log('Scanner closed');
});
errorCustomEvent<unknown>

Emitted when something fails: the helper service is unreachable, the scanner does not respond, the licence is rejected, or the file cannot be produced. The error itself arrives as event.detail. Use it to log the failure on your side or to show your own message. The widget also writes the error to the browser console.

example
scanner.addEventListener('error', (event) => {
  console.error(event.detail);
});

Callback properties

The widget also supports callback properties: onSaved, onClosed and onError.

onSaved
(detail: {
  fileType: 'pdf' | 'png' | 'jpg';
  blob?: Blob;
  pages?: Array<{
    id: string;
    name: string;
    url: string;
    mimeType: string;
    blob: Blob;
  }>;
}) => void
onClosed
scanner.onClosed = () => {
  console.log('Scanner closed');
};

Notes

  • The widget automatically looks for the local scanner service on the supported local ports.
  • When it finds a working port, it remembers it in localStorage and reuses it on the next load.

Troubleshooting

Scanner service

  • If the scanner service is not running, the widget shows the install/help modal automatically.
  • Start or install the local scanner service, then try again.
  • If needed, reload the page after the service starts so the widget can check the local ports again.

License

  • "License expired" / scanning flow will not open: the license has expired. Renew it at scanbridge.dev; active machines recover within a few seconds.
  • "License not allowed" on a specific site: the current domain is not on the license's allow-list. Add your production origin to the license.
  • Nothing happens after setting license-key: double-check the key value and that license enforcement is expected for your build. The helper validates the key with the licensing server on first use.
  • Verify locally (advanced): with the helper running you can check http://127.0.0.1:18625/ready. It returns a single verdict like { "ready": true } or { "ready": false, "reason": "license_expired" }.

Still stuck?

If the problem still continues, contact support by email.