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
v1.4+Use 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
v1.4+The 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-setup-complete', (e) => {
console.log('2FA enabled for:', e.detail.email);
});
document.addEventListener('otp-verified', (e) => {
window.location.href = '/dashboard'; // proceed with login
});
</script>🔒 Securing Your API Token
Importantapi-token="your-token" as a static HTML attribute makes it visible to anyone who views the page source or opens DevTools → Elements. Anyone can copy it and make API calls directly to your account.Your token lives only in your server's environment variables. Your backend exposes a session-gated endpoint that returns the token only to authenticated users. Your frontend JS fetches it and sets it on the widget — the static HTML never contains the token.
# Create this token in your OTP API dashboard → Origins → API Tokens # Never commit this file. Add .env to your .gitignore. OTP_API_TOKEN=your-token-from-dashboard
// routes/otp.js
// requireAuth = your existing session/JWT middleware
app.get('/otp-token', requireAuth, (req, res) => {
res.json({ token: process.env.OTP_API_TOKEN });
});// routes/web.php
Route::get('/otp-token', function () {
return response()->json([
'token' => config('services.otp_api.token')
]);
})->middleware('auth');// app/api/otp-token/route.ts
import { getServerSession } from 'next-auth';
import { NextResponse } from 'next/server';
export async function GET() {
const session = await getServerSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ token: process.env.OTP_API_TOKEN });
}# views.py
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
import os
@login_required
def otp_token(request):
return JsonResponse({'token': os.environ.get('OTP_API_TOKEN')})api-token attribute in the HTML. The attribute is only set after the authenticated fetch succeeds.<!-- otp-widget.js loaded in <head> -->
<script src="https://otp.royaltycoding.com/otp-widget.js"></script>
<!-- No api-token here — token is injected by JS below -->
<otp-setup-btn
id="otp-widget"
api="https://otp.royaltycoding.com"
email="user@example.com">
</otp-setup-btn>
<script>
(async function() {
try {
// Fetch token from your server — only works if user is logged in
const res = await fetch('/otp-token');
if (!res.ok) return; // user not authenticated — widget stays inactive
const { token } = await res.json();
// Set on all OTP widgets on the page
document.querySelectorAll(
'otp-setup-btn, otp-verify-btn, otp-disable-btn'
).forEach(el => el.setAttribute('api-token', token));
} catch (e) {
console.error('Could not load OTP token:', e);
}
})();
</script>import { useEffect, useRef } from 'react';
function OtpSetupButton({ email }) {
const ref = useRef(null);
useEffect(() => {
async function injectToken() {
const res = await fetch('/otp-token');
if (!res.ok) return;
const { token } = await res.json();
if (ref.current) ref.current.setAttribute('api-token', token);
}
injectToken();
}, []);
return (
<otp-setup-btn
ref={ref}
api="https://otp.royaltycoding.com"
email={email}
/>
);
}| Threat | Protected? | How |
|---|---|---|
| Token visible in HTML source | ✅ Blocked | Token injected via JS after page load — never in static HTML |
| Unauthenticated user fetches token | ✅ Blocked | Your /otp-token endpoint requires an active session |
| Token used from another website | ✅ Blocked | Token is origin-scoped — rejected if Origin header doesn't match |
| Token leaked via git / source control | ✅ Blocked | Token lives only in .env which is in .gitignore |
| Token visible in CDN / proxy cache | ✅ Blocked | Token is in a dynamic authenticated response, not static HTML |
| Token visible in DevTools Network tab | ⚠️ Unavoidable | Any authenticated user on their own device can see it — this is the browser's limit. Mitigated by origin-scoping + rate limits |
| Brute-force / abuse via stolen token | ✅ Limited | Rate-limited to 10 verify attempts per 5 min per IP via DDoS config |