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:
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):
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:
{
"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"
}
}
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.
- Install the package.
- Import it once, for example in
main.ts. - Allow the custom element with
CUSTOM_ELEMENTS_SCHEMA.
import '@scanner/web';
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 {}
- Install the package.
- Import it once in your app entry or component.
- Render the custom element directly.
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):
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.
'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.
- Install the package.
- Import it once, for example in
main.ts. - Make sure Vue treats it as a custom element if needed in your setup.
import '@scanner/web';
<template>
<scanner-widget license-key="SB-XXXX-XXXX-XXXX" />
</template>
<script lang="ts">
import '@scanner/web';
</script>
<scanner-widget license-key="SB-XXXX-XXXX-XXXX" />
<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
Controls the output file type.
<scanner-widget file-type="pdf"></scanner-widget>
theme
Chooses between the light and dark appearance.
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.
<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.
<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.
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
Controls the widget language.
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.
<scanner-widget lang="en"></scanner-widget>
open
Controls whether the widget modal is visible. Default: true.
<scanner-widget open></scanner-widget>
license-key
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.
<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:
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
- Render
<scanner-widget>. - Optionally set
file-type,lang, oropen. - Listen for
savedandclosed, or useonSavedandonClosed.
Example with addEventListener
<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
<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.
savedEmitted after save finishes successfully.
CustomEvent<
| { fileType: 'pdf'; blob: Blob }
| {
fileType: 'png' | 'jpg';
pages: Array<{
id: string;
name: string;
url: string;
mimeType: string;
blob: Blob;
}>;
}
>
scanner.addEventListener('saved', (event) => {
const detail = event.detail;
if (detail.fileType === 'pdf') {
console.log(detail.blob);
return;
}
console.log(detail.pages);
});
closedEmitted when the widget is closed.
scanner.addEventListener('closed', () => {
console.log('Scanner closed');
});
errorEmitted 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.
scanner.addEventListener('error', (event) => {
console.error(event.detail);
});
Callback properties
The widget also supports callback properties: onSaved, onClosed and onError.
(detail: {
fileType: 'pdf' | 'png' | 'jpg';
blob?: Blob;
pages?: Array<{
id: string;
name: string;
url: string;
mimeType: string;
blob: Blob;
}>;
}) => void
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
localStorageand 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.