Two-factor / OTP block
The verification step of a sign-in flow: SvOtpInput (6 digits, auto-advance + onComplete), a resend cooldown timer, and the waiting -> verifying -> verified states. Enter 000000 to see the error path.
A live, editable Svelte 5 component example from the SvGrid gallery (Blocks). Read the SvGrid UI component docs for the full API.
About this example
A copy-paste two-factor verification block built from the SvGrid UI kit in Svelte 5: a centred card with SvOtpInput for six digits that auto-advances and fires onComplete, a resend cooldown timer, verify and back actions, and the waiting, verifying and verified states with a loading button. Enter 000000 to see the error path.
Two-factor verification block - the OTP step of a sign-in flow. A centered card with SvOtpInput (6 digits, auto-advance + onComplete), a resend timer, and verify / back actions. Shows the "waiting -> verifying -> done" states. Pure UI-kit, --sg-* themed.
Imports, features and API used
Imports: @svgrid/grid
Frequently asked questions
How does verification start?
onComplete fires when the sixth digit is entered and calls verify with the code, so most users never press the button; the button remains for a partial code.
How does the resend timer work?
A countdown in state decrements every second after a resend; the link is disabled and shows the remaining seconds until it reaches zero.
How is an error shown?
A rejected code sets the status to error, marks the input invalid with a message and clears it for another attempt; the demo rejects 000000.
Related documentation
Source code (422-block-otp.svelte)
<script lang="ts">
/**
* Two-factor verification block - the OTP step of a sign-in flow. A centered
* card with SvOtpInput (6 digits, auto-advance + onComplete), a resend timer,
* and verify / back actions. Shows the "waiting -> verifying -> done" states.
* Pure UI-kit, --sg-* themed.
*/
import {
SvCard, SvOtpInput, SvButton, SvBadge, SvDivider, SvToaster, toast,
} from '@svgrid/grid'
let code = $state('')
let status = $state<'idle' | 'verifying' | 'done'>('idle')
let cooldown = $state(0)
const dest = '[email protected]'
function verify(value = code) {
if (value.length < 6) { toast('Enter all 6 digits', { variant: 'warning' }); return }
status = 'verifying'
setTimeout(() => {
if (value === '000000') { status = 'idle'; toast.error('Invalid code, try again'); return }
status = 'done'
toast.success('Identity verified')
}, 900)
}
let timer: ReturnType<typeof setInterval> | undefined
function resend() {
if (cooldown > 0) return
toast('New code sent to ' + dest)
cooldown = 30
timer = setInterval(() => {
cooldown -= 1
if (cooldown <= 0 && timer) clearInterval(timer)
}, 1000)
}
</script>
<div class="otp-wrap">
<SvCard>
<div class="head">
<span class="shield">🛡</span>
<h2>Verify it's you</h2>
<p class="muted">We sent a 6-digit code to <strong>{dest}</strong>.</p>
</div>
{#if status === 'done'}
<div class="done">
<span class="check">✓</span>
<div>
<strong>Verified</strong>
<div class="muted">Redirecting to your dashboard...</div>
</div>
</div>
{:else}
<div class="otp">
<SvOtpInput
bind:value={code}
length={6}
autofocus
onComplete={(v) => verify(v)}
/>
</div>
<SvButton
variant="primary"
block
loading={status === 'verifying'}
onclick={() => verify()}
>Verify</SvButton>
<div class="resend">
<span class="muted">Didn't get a code?</span>
{#if cooldown > 0}
<SvBadge variant="neutral">Resend in {cooldown}s</SvBadge>
{:else}
<button class="link" onclick={resend}>Resend code</button>
{/if}
</div>
{/if}
{#snippet footer()}
<div class="foot">
<button class="link" onclick={() => toast('Back to sign-in')}>← Use a different account</button>
<button class="link" onclick={() => toast('Recovery options opened')}>Trouble signing in?</button>
</div>
{/snippet}
</SvCard>
<p class="hint">Tip: enter <code>000000</code> to see the error state.</p>
</div>
<SvToaster position="bottom-right" />
<style>
.otp-wrap { padding: 32px 24px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* Scoped to this demo's unique wrapper class so the global card-width rule
can't leak onto other demos that also use a `.wrap` container. */
.otp-wrap :global(> *:first-child) { width: 100%; max-width: 400px; }
.head { text-align: center; margin-bottom: 18px; }
.shield { font-size: 30px; display: block; margin-bottom: 8px; }
.head h2 { margin: 0 0 4px; font-size: 21px; }
.muted { color: var(--sg-muted, #64748b); font-size: 13.5px; margin: 0; }
.otp { display: flex; justify-content: center; margin: 6px 0 18px; }
.resend { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 14px; font-size: 13px; }
.link { background: none; border: 0; padding: 0; font: inherit; color: var(--sg-accent, #4f46e5); cursor: pointer; }
.link:hover { text-decoration: underline; }
.foot { display: flex; flex-direction: column; gap: 6px; align-items: center; font-size: 12.5px; }
.done { display: flex; align-items: center; gap: 14px; justify-content: center; padding: 18px 0; }
.check { display: inline-flex; width: 40px; height: 40px; align-items: center; justify-content: center; border-radius: 50%; background: color-mix(in srgb, #22c55e 20%, transparent); color: #22c55e; font-size: 20px; font-weight: 700; }
.done strong { font-size: 15px; }
.hint { font-size: 12px; color: var(--sg-muted, #94a3b8); margin: 0; }
.hint code { background: var(--sg-row-hover-bg, #f1f5f9); padding: 1px 5px; border-radius: 4px; }
</style>More Blocks examples
- Login block - A two-column authentication page: a branded gradient panel beside a sign-in SvCard with social SvButtons, SvTextInput + SvPasswordInput (reveal), remember-me SvCheckBox, an "or" SvDivider and forgot-password link. Drops into any route as-is.
- Sign-up block - A centered account-creation card: two-up name fields, work email, a password with the live strength meter, a plan SvSegmented switch, terms SvCheckBox and a benefits rail. Submit fires a promise toast. Pure UI-kit.
- Analytics dashboard block - A full overview page: a range SvSegmented + actions header, a KPI grid (SvStat + SvSparkline), a revenue bar panel, a recent-activity SvTimeline and a top-products list with SvProgress bars. No grid dependency.
- App shell block - A complete application layout: a SvNavPane sidebar with sections + badges, a top bar with SvBreadcrumb, search and an account SvMenu, and a routed content area. Nav swaps the page; the toggle collapses the rail to an icon rail.
- Pricing block - A three-tier plan grid with a monthly/annual SvSegmented toggle (annual applies the discount live), a highlighted "most popular" card, per-plan feature SvLists with check marks, and a CTA SvButton on each.