wts-otp
v20.0.1
Published
Zero-dependency, accessible OTP, PIN, and verification-code input for JavaScript, Angular, React, Vue, Svelte, and Web Components.
Maintainers
Readme
wts-otp
A zero-dependency, accessible OTP, PIN, and verification-code input for vanilla JavaScript, Angular, React, Vue, Svelte, and other browser frameworks. Use the core DOM API or the optional form-associated Web Component without adding a framework runtime dependency.
wts-otp provides the user-interface control for entering a code. It does not
generate or verify TOTP, HOTP, SMS, or authentication codes.
Install
npm install wts-otpCore API
<div id="verification-code"></div>import { WtsOtp } from 'wts-otp';
const otp = new WtsOtp('#verification-code', {
autoFill: true,
clipboardEnabled: true,
digits: 6,
type: 'number',
visibleAs: 'text',
onChange: (value) => console.log('Changed:', value),
onComplete: (value) => console.log('Complete:', value),
});
otp.value = '123';
otp.focus(3);
otp.reset();
// Call when the owning screen or component is removed.
otp.destroy();WtsOtp accepts a selector, an Element, or a ShadowRoot. The package does
not access the browser DOM until an instance is constructed, so importing it
during server-side rendering is safe.
Web Component
The Web Component is optional and uses a separate, explicit entry point:
import 'wts-otp/element';<wts-otp
digits="6"
auto-fill="false"
clipboard-enabled="false"
type="number"
visible-as="password"
name="verificationCode"
required
aria-label="Verification code"
aria-describedby="verification-error"
></wts-otp>
<p id="verification-error">Enter all six digits.</p>Listen for input, change, and complete:
const element = document.querySelector('wts-otp');
element.addEventListener('complete', (event) => {
console.log(event.detail.value);
});The element supports value, complete, autoFill, clipboardEnabled,
disabled, readOnly, required, focus(), focusDigit(index),
reset(), checkValidity(), and reportValidity(). It participates in
FormData, form reset, disabled fieldsets, and native required validation.
Tooling and IDEs can discover the Web Component API through the published Custom Elements Manifest:
import manifest from 'wts-otp/custom-elements.json' with { type: 'json' };Framework integration
Create the core instance after the framework has mounted the host element and
call destroy() during cleanup.
React
function OtpInput() {
const host = useRef<HTMLDivElement>(null);
useEffect(() => {
const otp = new WtsOtp(host.current!, { digits: 6 });
return () => otp.destroy();
}, []);
return <div ref={host} />;
}Vue
const host = ref<HTMLElement>();
let otp: WtsOtp;
onMounted(() => {
otp = new WtsOtp(host.value!, { digits: 6 });
});
onUnmounted(() => otp.destroy());The same lifecycle pattern works in Angular, Svelte, Solid, and other browser frameworks. No framework adapter is required.
Options
| Option | Default | Purpose |
| --- | --- | --- |
| digits | 4 | Number of OTP characters, from 1 to 32 |
| type | "number" | Numeric or unrestricted text input |
| visibleAs | "text" | Display characters as text or password |
| placeholder | "" | Placeholder for empty inputs |
| borderOn | "all-side" | Visible border side |
| autofocus | false | Focus the first empty character after mounting |
| autocomplete | "one-time-code" | Browser autofill hint for the first input |
| autoFill | true | Allow browser and device verification-code autofill suggestions |
| clipboardEnabled | true | Allow copy, cut, and paste interactions |
| value | "" | Initial value |
| disabled | false | Disable the input group |
| readOnly | false | Prevent user edits without disabling the group |
| required | false | Require every character for native validation |
| showError | false | Apply invalid state and styling |
| name | "otp" | Native form field name |
| ariaLabel | "One-time password" | Accessible group label |
| ariaDescribedBy | "" | IDs of descriptions or error messages |
| injectStyles | true | Inject default scoped styles |
Methods
otp.setValue('1234');
otp.setOptions({ digits: 6, showError: true });
otp.setDisabled(true);
otp.setError(true);
otp.focus(0);
otp.reset();
otp.destroy();setValue(value, true) emits both the input change and completion callback
when the new value is complete. Multi-character values delivered by OTP
autofill or paste fill from the first character even when another character is
focused. A one-character paste replaces only the focused character. Pasted
content with no valid characters leaves the current value unchanged.
Set clipboardEnabled: false to prevent copy, cut, and paste. Set
autoFill: false to apply autocomplete="off" to every character input. The
equivalent Web Component attributes are clipboard-enabled="false" and
auto-fill="false". Verification-code suggestions remain controlled by the
browser and operating system.
Styling
Default styles are injected when an instance is created. Set
injectStyles: false and import wts-otp/styles.css when inline styles are
restricted by a Content Security Policy.
Both the new prefixed variables and the original variables are supported:
.verification-code {
--wts-otp-border: 2px solid #545353;
--wts-otp-border-radius: 8px;
--wts-otp-input-width: 52px;
--wts-otp-input-height: 52px;
--wts-otp-gap: 8px;
--wts-otp-focus-border-color: #2563eb;
--wts-otp-invalid-border-color: #dc2626;
}Browser and release policy
The distributed ESM and CommonJS bundles target ES2020. Before publishing a release, run:
npm run test:browser:install
npm run checkDevelopment and browser testing use Node.js 22. npm run check runs the unit
suite, creates the distribution bundles, and executes Playwright in Chromium,
Firefox, and WebKit. To run only the browser suite or open Playwright's
interactive runner:
npm run test:browser
npm run test:browser:uiThe unit suite covers DOM behavior, Web Component lifecycle, native form fallback, accessibility state, autofill, paste, and packaging style consistency. The real-browser suite also covers reconnects, fieldset disabled state, validation recovery, focus preservation, reset behavior, narrow mobile layout, and paste distribution. The same matrix runs automatically in CI for changes to this package.
Supply-chain security
The package has no runtime dependencies. Development dependencies are locked
with package-lock.json, CI installs them with npm ci, and release builds
disable dependency lifecycle scripts during installation.
Production releases use a short-lived, package-scoped granular npm token stored only in a protected GitHub deployment environment. The release workflow:
- runs vulnerability and registry-signature audits;
- runs all unit and real-browser tests before packaging;
- separates unprivileged verification from the token-enabled publish job;
- transfers only the verified package tarball to the publish job;
- uses read-only GitHub permissions;
- pins every GitHub Action to a full commit SHA; and
- requires a matching version tag and protected
npmdeployment environment.
See RELEASING.md for the required one-time npm and GitHub configuration. Report vulnerabilities according to SECURITY.md.
Migration
Version 20 replaces the former Angular component with the framework-agnostic core. See MIGRATION.md for the v19 to v20 migration.
