OTP Web Components
Drop-in TOTP components — no build step, no framework required. Just include the script tag and use the custom HTML elements anywhere on your page.
⚡ Installation
<script> tag before your closing </body> tag — or in <head> with defer.<script src="https://otp.royaltycoding.com/otp-widget.js"></script>
<!-- On your account settings page --> <otp-setup-btn email="user@example.com" api="https://otp.royaltycoding.com" api-token="your-token-here" theme="dark"> </otp-setup-btn> <!-- On your login page --> <otp-verify-btn email="user@example.com" api="https://otp.royaltycoding.com"> </otp-verify-btn> <!-- On your security settings page --> <otp-disable-btn email="user@example.com" api="https://otp.royaltycoding.com"> </otp-disable-btn>
document level.document.addEventListener('otp-setup-complete', (e) => {
console.log('2FA enabled for:', e.detail.email);
// Reload user session, show success toast, etc.
});
document.addEventListener('otp-verified', (e) => {
console.log('Verified:', e.detail.email);
// Redirect to protected page
window.location.href = '/dashboard';
});
document.addEventListener('otp-disabled', (e) => {
console.log('2FA removed for:', e.detail.email);
});
document.addEventListener('otp-error', (e) => {
const { message, httpStatus, errorType } = e.detail;
// errorType: "rate" | "cors" | "apierr" | "network"
console.error('[otp-error]', errorType, httpStatus, message);
});allowed_origins table or all API calls will return 403. See the Security Guide for details.INSERT INTO allowed_origins (origin) VALUES ('https://yourdomain.com');🧩 Components Overview
otp-setup-complete
otp-verified
otp-disabled
📋 All Attributes
All three components share the same attribute interface. All attributes are optional — sensible defaults are applied for each.
| Attribute | Type | Default | Components | Description |
|---|---|---|---|---|
| string | "" | all | Pre-fills the email input and skips the email step entirely when provided. The user jumps straight to the QR / code entry step. Pass this from your server after login. | |
| api | string | https://otp.royaltycoding.com | all | Base URL for the OTP API. Override this if you self-host the backend on a different domain or port. |
| theme | "dark" | "light" | "dark" | all | Controls the modal overlay appearance. "dark" = dark background with light text. "light" = white background with dark text. |
| label | string | component default | all | Overrides the button text. Defaults: otp-setup-btn → "Enable 2FA", otp-verify-btn → "Verify 2FA Code", otp-disable-btn → "Disable 2FA". |
| icon | string | "" | component default | all | Overrides the button icon. Any emoji, SVG string, or text. Set icon="" (empty string) to hide the icon completely. Omitting the attribute uses the default icon. Live-reactive: updating the attribute re-renders the button. |
| api-token | string | "" | all | Required. Bearer token for API authentication. Sent as Authorization: Bearer <token> on every request. Create tokens via /admin/tokens. If missing or invalid the widget shows a 401 error banner. Never hard-code this in public HTML — set it dynamically from your server-rendered page. |
label and icon attributes are observed — changing them via JavaScript (el.setAttribute('icon', '🛡️')) will immediately re-render the button without needing a full page refresh.📡 Events
All events bubble (bubbles: true) and are composed (composed: true), so they cross shadow DOM boundaries and can be caught at any ancestor level.
| Event | Fired by | detail payload | When |
|---|---|---|---|
| otp-setup-complete | setup | { email, secret } |
TOTP was successfully configured and enabled on the account. |
| otp-verified | verify | { email } |
A valid 6-digit code was accepted by the API at login. |
| otp-disabled | disable | { email } |
TOTP was successfully removed from the account. |
| otp-error | all | { message, httpStatus, errorType } |
Any API or network error occurred. errorType: "rate" | "cors" | "apierr" | "network". |
🔐 Setup Button — Live Demos
otp-setup-btnOpens a 3-step modal: enter email → scan QR code → enter confirmation code. On success, TOTP is enabled for that (email, origin) pair.
<otp-setup-btn api="https://otp.royaltycoding.com"> </otp-setup-btn>
<otp-setup-btn email="alice@example.com" api="https://otp.royaltycoding.com"> </otp-setup-btn>
<otp-setup-btn api="https://otp.royaltycoding.com" theme="light"> </otp-setup-btn>
🛡️ Verify Button — Live Demos
otp-verify-btnOpens a 2-step modal for login verification. When email is provided it skips straight to the code-entry step (typical login use case).
<otp-verify-btn api="https://otp.royaltycoding.com"> </otp-verify-btn>
<otp-verify-btn email="alice@example.com" api="https://otp.royaltycoding.com" api-token="your-token-here"> </otp-verify-btn>
<otp-verify-btn api="https://otp.royaltycoding.com" theme="light"> </otp-verify-btn>
🔓 Disable Button — Live Demos
otp-disable-btnRequires the user to enter a valid current TOTP code before 2FA is removed — prevents unauthorized removal.
<otp-disable-btn api="https://otp.royaltycoding.com"> </otp-disable-btn>
<otp-disable-btn email="alice@example.com" api="https://otp.royaltycoding.com" api-token="your-token-here"> </otp-disable-btn>
<otp-disable-btn api="https://otp.royaltycoding.com" theme="light"> </otp-disable-btn>
🌗 Dark & Light Themes
All three components support both theme="dark" (default) and theme="light". The modal overlay, form fields, error banners, and button states all respond to the theme.
<!-- Dark modal (default — no attribute needed) --> <otp-setup-btn api="https://otp.royaltycoding.com"></otp-setup-btn> <otp-setup-btn api="https://otp.royaltycoding.com" theme="dark"></otp-setup-btn> <!-- Light modal --> <otp-setup-btn api="https://otp.royaltycoding.com" theme="light"></otp-setup-btn>
🎨 Icon Variants
New in v1.4Use the icon attribute to replace the default icon with any emoji, text, or SVG. Set icon="" (empty string) to hide the icon entirely. Omitting the attribute keeps the default.
<otp-setup-btn icon="🔑" ...></otp-setup-btn> <otp-verify-btn icon="✅" ...></otp-verify-btn> <otp-disable-btn icon="⛔" ...></otp-disable-btn>
icon=""<!-- Empty string hides icon completely --> <otp-setup-btn icon="" ...></otp-setup-btn> <otp-verify-btn icon="" ...></otp-verify-btn> <otp-disable-btn icon="" ...></otp-disable-btn>
📧 Pre-filled Email
When the email attribute is set, the email-entry step is skipped entirely and the modal opens directly at the action step. This is the recommended usage when the user is already logged in.
<!-- Typical server-rendered pattern: -->
<otp-setup-btn email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-setup-btn>
<otp-verify-btn email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-verify-btn>
<otp-disable-btn email="{{ user.email }}" api="https://otp.royaltycoding.com"></otp-disable-btn>email attribute from your templating engine (Handlebars, Blade, Jinja, etc.). This avoids the extra email step and provides a seamless UX.✏️ Custom Labels
Override button text with the label attribute. Combine with icon for fully customised buttons that match your design language.
<otp-setup-btn label="Activate 2FA" ...></otp-setup-btn> <otp-verify-btn label="Enter Code" ...></otp-verify-btn> <otp-disable-btn label="Remove 2FA" ...></otp-disable-btn>
<otp-setup-btn icon="🔑" label="Set Up Authenticator" ...></otp-setup-btn> <otp-verify-btn icon="✔️" label="Confirm Identity" ...></otp-verify-btn> <otp-disable-btn icon="🗑️" label="Turn Off 2FA" ...></otp-disable-btn>
⚠️ Error Banners
New in v1.4The widget automatically shows contextual error banners inside the modal for all API and network failures. There are four error types, each with distinct styling and messaging.
document.addEventListener('otp-error', (e) => {
const { message, httpStatus, errorType } = e.detail;
switch (errorType) {
case 'rate': // 429 — rate limited
showToast('Too many attempts. Please wait.', 'warning');
break;
case 'cors': // 403 — origin not allowed
showToast('Domain not authorized.', 'error');
break;
case 'network': // 0 — no connection
showToast('Network error. Check connection.', 'error');
break;
case 'apierr': // other 4xx/5xx
showToast('API error: ' + message, 'error');
break;
}
});📡 Live Event Log
Interact with any widget on this page — all events are captured and logged below in real time.
['otp-setup-complete', 'otp-verified', 'otp-disabled', 'otp-error'].forEach(name => {
document.addEventListener(name, (e) => {
console.log(name, e.detail);
});
});🏗️ Combined Example — Security Settings Page
A realistic example showing how all three buttons work together on a user security settings page, with event handling.
<!DOCTYPE html>
<html>
<head>
<script src="https://otp.royaltycoding.com/otp-widget.js" defer></script>
</head>
<body>
<div class="settings-panel">
<!-- Enable 2FA -->
<div class="setting-row">
<div class="setting-info">
<h4>Two-Factor Authentication</h4>
<p>Protect your account with an authenticator app</p>
</div>
<otp-setup-btn
email="{{ user.email }}"
api="https://otp.royaltycoding.com"
icon="🔑"
label="Enable 2FA">
</otp-setup-btn>
</div>
<!-- Verify identity -->
<div class="setting-row">
<otp-verify-btn
email="{{ user.email }}"
api="https://otp.royaltycoding.com">
</otp-verify-btn>
</div>
<!-- Remove 2FA (danger zone) -->
<div class="setting-row danger">
<otp-disable-btn
email="{{ user.email }}"
api="https://otp.royaltycoding.com"
icon="🗑️"
label="Remove 2FA">
</otp-disable-btn>
</div>
</div>
<script>
document.addEventListener('otp-setup-complete', (e) => {
alert('2FA enabled for: ' + e.detail.email);
location.reload(); // Refresh to show 2FA status
});
document.addEventListener('otp-verified', (e) => {
window.location.href = '/dashboard';
});
document.addEventListener('otp-disabled', (e) => {
alert('2FA removed from your account.');
location.reload();
});
document.addEventListener('otp-error', (e) => {
console.error('[otp-error]', e.detail);
});
</script>
</body>
</html>🔴 Error Type Reference
| errorType | HTTP Status | Cause | UX Behaviour |
|---|---|---|---|
| rate | 429 | Too many requests — DDoS protection triggered | Fatal banner replaces modal body. User must close and wait. |
| cors | 403 | Origin not in allowed_origins table |
Fatal banner. Domain must be whitelisted by admin. |
| apierr | 4xx / 5xx | Wrong code, user not found, server error | Digit inputs shake and reset. User can retry immediately. |
| network | 0 (no response) | Fetch failed — offline, DNS, timeout | Fatal banner with network troubleshooting hint. |
⚙️ Framework Integration Examples
// In your <head> or main entry:
// <script src="https://otp.royaltycoding.com/otp-widget.js"></script>
import { useEffect } from 'react';
export default function SecuritySettings({ user }) {
useEffect(() => {
const onSetup = (e) => console.log('2FA on:', e.detail.email);
const onVerify = (e) => window.location.href = '/dashboard';
const onError = (e) => console.error('OTP error:', e.detail);
document.addEventListener('otp-setup-complete', onSetup);
document.addEventListener('otp-verified', onVerify);
document.addEventListener('otp-error', onError);
return () => {
document.removeEventListener('otp-setup-complete', onSetup);
document.removeEventListener('otp-verified', onVerify);
document.removeEventListener('otp-error', onError);
};
}, []);
return (
<div className="security-settings">
<otp-setup-btn
email={user.email}
api="https://otp.royaltycoding.com"
theme="dark"
/>
<otp-verify-btn
email={user.email}
api="https://otp.royaltycoding.com"
/>
<otp-disable-btn
email={user.email}
api="https://otp.royaltycoding.com"
icon="⛔"
label="Remove 2FA"
/>
</div>
);
}<template>
<div>
<otp-setup-btn
:email="user.email"
api="https://otp.royaltycoding.com"
theme="dark"
/>
<otp-verify-btn
:email="user.email"
api="https://otp.royaltycoding.com"
/>
<otp-disable-btn
:email="user.email"
api="https://otp.royaltycoding.com"
/>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps(['user']);
const onSetup = (e) => console.log('2FA enabled:', e.detail);
const onVerify = (e) => console.log('Verified:', e.detail);
const onError = (e) => console.error('OTP error:', e.detail);
onMounted(() => {
document.addEventListener('otp-setup-complete', onSetup);
document.addEventListener('otp-verified', onVerify);
document.addEventListener('otp-error', onError);
});
onUnmounted(() => {
document.removeEventListener('otp-setup-complete', onSetup);
document.removeEventListener('otp-verified', onVerify);
document.removeEventListener('otp-error', onError);
});
</script><!-- In your Blade layout head -->
<script src="https://otp.royaltycoding.com/otp-widget.js" defer></script>
<!-- In your security settings view -->
<otp-setup-btn
email="{{ auth()->user()->email }}"
api="https://otp.royaltycoding.com"
theme="dark">
</otp-setup-btn>
<otp-verify-btn
email="{{ auth()->user()->email }}"
api="https://otp.royaltycoding.com">
</otp-verify-btn>
<otp-disable-btn
email="{{ auth()->user()->email }}"
api="https://otp.royaltycoding.com"
icon="⛔"
label="Remove 2FA">
</otp-disable-btn>
<script>
document.addEventListener('otp-verified', (e) => {
window.location.href = '{{ route("dashboard") }}';
});
</script>// Fetch user info and set email dynamically
const user = await fetch('/api/me').then(r => r.json());
document.querySelectorAll('otp-setup-btn, otp-verify-btn, otp-disable-btn')
.forEach(el => el.setAttribute('email', user.email));
// You can also update icon or label at runtime:
const setupBtn = document.querySelector('otp-setup-btn');
setupBtn.setAttribute('icon', user.hasTOTP ? '✅' : '🔐');
setupBtn.setAttribute('label', user.hasTOTP ? '2FA Active' : 'Enable 2FA');