New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@verino/svelte

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@verino/svelte

Svelte adapter for Verino. Reliable OTP inputs from a single core.

latest
Source
npmnpm
Version
2.1.0
Version published
Maintainers
1
Created
Source
verino

@verino/svelte

Svelte adapter for Verino. Reliable OTP inputs from a single core.

Live demo  npm version  gzip size  Zero dependencies  TypeScript

Overview

@verino/svelte wraps @verino/core using Svelte's native primitives. State is exposed as writable and derived stores, every property you need in a template is available with the $ prefix, no manual subscription code required.

The centerpiece is use:otp.action, a Svelte action placed on the hidden <input>. It wires all keyboard, paste, focus, and blur events to the core machine, starts the timer on mount, and calls destroy() automatically when the element is removed. Visual slot divs are purely decorative mirrors, hold no event listeners and carry no state of their own.

The store structure follows one key rule: otp is a store ($otp gives the full OTPStateSnapshot). All other stores (slots, wrapperAttrs, timerSeconds, masked, separatorAfter, etc.) must be destructured from the return value before subscribing with $.

Use value for live external control with a Svelte readable store. Use defaultValue for one-time prefill on mount.

Why Use This Adapter?

  • Native Svelte stores. State is exposed as writable and derived — use $ subscription with no boilerplate.
  • use: action. One directive wires the hidden input, timer, and cleanup lifecycle.
  • SvelteKit compatible. SSR-safe — the DOM adapter only runs on the client.
  • Full markup control. No opaque component wrapper.

Installation

# npm
npm i @verino/svelte

# pnpm
pnpm add @verino/svelte

# yarn
yarn add @verino/svelte

Peer dependency: Svelte ≥ 4. @verino/core installs automatically.

Quick Start

<script>
  import { useOTP } from '@verino/svelte'

  const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
  const { slots, wrapperAttrs } = otp
</script>

<div {...$wrapperAttrs} style="position: relative; display: inline-flex; gap: 8px">
  <input
    use:otp.action
    style="position: absolute; inset: 0; opacity: 0; z-index: 1; cursor: text"
  />

  {#each $slots as slot (slot.index)}
    <div
      aria-hidden="true"
      class="slot"
      class:is-active={slot.isActive}
      class:is-filled={slot.isFilled}
      class:is-error={$otp.hasError}
    >
      {#if slot.isActive && !slot.isFilled}
        <span class="caret" />
      {/if}
      {slot.isFilled ? slot.value : otp.placeholder}
    </div>
  {/each}
</div>

Note: verify(code) and similar functions used in examples are placeholders — replace them with your own API calls or application logic.

Common Patterns

Use caseKey options
SMS / email OTPtype: 'numeric', timer: 60, onResend
TOTP / 2FA with separatorseparatorAfter: 3
PIN entrymasked: true, blurOnComplete: true
Alphanumeric codetype: 'alphanumeric', pasteTransformer
Invite / referral codeseparatorAfter: [3, 6], pattern: /^[A-Z0-9]$/
Hex activation keypattern: /^[0-9A-F]$/
Async verification locksetDisabled(true / false) around the API call
Native form submissionname: 'otp_code'
Pre-fill on mountdefaultValue: '123456'
Display-only / read-onlyreadOnly: true

Usage

Live external control

Use value for live external control and defaultValue for one-time mount prefill. In Svelte, live external control means passing a readable store:

<script>
  import { writable } from 'svelte/store'

  const code = writable('')
  const otp = useOTP({ length: 6, value: code, onChange: (v) => code.set(v) })
</script>

Use setValue() when you want a one-off programmatic fill without turning the field into a controlled store:

<script>
  const otp = useOTP({ length: 6 })

  function prefill() { otp.setValue('123456') }
  function clear()   { otp.setValue('') }
</script>

Propagate user keystrokes back to an external variable via onChange:

<script>
  let code = ''
  const otp = useOTP({ length: 6, onChange: (v) => (code = v) })
</script>

Async verification

setDisabled updates both the store and the bound input element atomically:

<script>
  const otp = useOTP({
    length: 6,
    onComplete: async (code) => {
      otp.setDisabled(true)
      const ok = await api.verify(code)
      otp.setDisabled(false)
      ok ? otp.setSuccess(true) : otp.setError(true)
    },
  })
</script>

Timer

timerSeconds is a Writable<number> — destructure it before subscribing:

<script>
  const otp = useOTP({ length: 6, timer: 60, onExpire: () => showExpired() })
  const { timerSeconds } = otp
</script>

{#if $timerSeconds > 0}
  <p>
    Expires in {Math.floor($timerSeconds / 60)}:{String($timerSeconds % 60).padStart(2, '0')}
  </p>
{/if}

Separator

separatorAfter is a Writable store. Build a reactive Set with $::

<script>
  const otp = useOTP({ length: 6, separatorAfter: 3, separator: '—' })
  const { slots, separatorAfter, separator } = otp

  $: sepSet = new Set(Array.isArray($separatorAfter) ? $separatorAfter : [$separatorAfter])
</script>

{#each $slots as slot (slot.index)}
  {#if sepSet.has(slot.index)}
    <span aria-hidden="true">{$separator}</span>
  {/if}
  <div
    aria-hidden="true"
    class="slot"
    class:is-active={slot.isActive}
    class:is-filled={slot.isFilled}
  >
    {slot.value}
  </div>
{/each}

Masked input

masked and maskChar are Writable stores — destructure them:

<script>
  const otp = useOTP({ length: 6, masked: true, maskChar: '●' })
  const { slots, masked, maskChar } = otp
</script>

{#each $slots as slot (slot.index)}
  <div aria-hidden="true" class="slot">
    {slot.isFilled ? ($masked ? $maskChar : slot.value) : otp.placeholder}
  </div>
{/each}

data-* state attributes

Spread data-* props onto slot divs for CSS driven state styling:

<script>
  function slotDataAttrs(index) {
    const props = otp.getInputProps(index)
    const out = {}
    for (const key in props) if (key.startsWith('data-')) out[key] = props[key]
    return out
  }
</script>

{#each $slots as slot (slot.index)}
  <div class="slot" aria-hidden="true" {...slotDataAttrs(slot.index)}>
    {slot.value}
  </div>
{/each}

Slot attributes

Slot-level attributes use string values ("true" / "false"):

AttributeMeaning
data-activeLogical cursor is at this slot (set even when the field is blurred)
data-focusBrowser focus is on the hidden input
data-filledSlot contains a character
data-emptySlot is unfilled (complement of data-filled)
data-invalidError state is active
data-successSuccess state is active (mutually exclusive with data-invalid)
data-disabledField is disabled
data-readonlyField is in read-only mode
data-completeAll slots are filled
data-firstThis is the first slot 0
data-lastThis is the last slot
data-slotZero-based position of the slot as a string ("0", "1", …)

Wrapper attributes

Set on the wrapper element as boolean presence attributes (no value):

AttributeWhen present
data-completeAll slots are filled
data-invalidError state is active
data-successSuccess state is active
data-disabledField is disabled
data-readonlyField is read-only
/* Slot-level — scope to your field with an id or class prefix */
.slot[data-active="true"][data-focus="true"] { border-color: #2A2A2A; }
.slot[data-filled="true"]                    { background:   #FFFFFF; }
.slot[data-empty="true"]                     { background:   #FAFAFA; }
.slot[data-invalid="true"]                   { border-color: #FF3846; }
.slot[data-success="true"]                   { border-color: #00C65B; }
.slot[data-disabled="true"]                  { opacity: 0.45; pointer-events: none; }
.slot[data-readonly="true"]                  { cursor: default; }
.slot[data-complete="true"]                  { border-color: #00C65B; }

/* Connected pill layout */
.slot[data-first="true"]                              { border-radius: 8px 0 0 8px; }
.slot[data-last="true"]                               { border-radius: 0 8px 8px 0; }
.slot[data-first="false"][data-last="false"]          { border-radius: 0; }

/* Target a specific slot by index */
.slot[data-slot="0"] { font-weight: 700; }

Spread $wrapperAttrs on the container for wrapper-level attributes:

<div {...$wrapperAttrs}>
  <!-- data-complete, data-invalid, data-success, data-disabled, data-readonly -->
</div>

CSS Custom Properties

Style the field using --verino-* CSS custom properties on the wrapper element:

.verino-wrapper  {
  /* Dimensions */
  --verino-size:          56px;
  --verino-gap:           12px;
  --verino-radius:        10px;
  --verino-font-size:     24px;

  /* Colors */
  --verino-bg:            #FAFAFA;
  --verino-bg-filled:     #FFFFFF;
  --verino-color:         #0C0C0C;
  --verino-border-color:  #DBDBDB;
  --verino-active-color:  #2A2A2A;
  --verino-error-color:   #FF3846;
  --verino-success-color: #00C65B;
  --verino-caret-color:   #2A2A2A;

  /* Placeholder, separator & mask */
  --verino-placeholder-color: #888888;
  --verino-placeholder-size:  16px;
  --verino-separator-color:   #B2B2B2;
  --verino-separator-size:    18px;
  --verino-masked-size:       16px;
}

Accessibility

  • Single ARIA-labelled input — the hidden input carries aria-label="Enter your N-digit code" (or N-character code for non-numeric types). Screen readers announce one field, not multiple slots.
  • All visual elements are aria-hidden — slots, separators, caret, and timer UI are removed from the accessibility tree.
  • inputMode — set to "numeric" or "text" based on type, triggering the correct mobile keyboard.
  • autocomplete="one-time-code" — enables native SMS autofill on iOS and Android.
  • Anti-interference — spellcheck="false", autocorrect="off", and autocapitalize="off" prevent unwanted browser input behavior.
  • maxLength — constrains the hidden input to length, preventing overflow from IME and composition events.
  • type="password" in masked mode — enables secure input and triggers the password keyboard on mobile.
  • Native form integration — the name option includes the hidden input in <form> submission and FormData.
  • Keyboard navigation — full support for ←, →, Backspace, Delete, and Tab.

API Reference

useOTP(options?)

function useOTP(options?: SvelteOTPOptions): UseOTPResult

SvelteOTPOptions

Builds on CoreOTPOptions from @verino/core with Svelte-specific state and rendering options:

type SvelteOTPOptions = CoreOTPOptions & {
  value?:          Readable<string>          // live external control via readable store
  onChange?:       (code: string) => void    // fires on INPUT, DELETE, CLEAR, PASTE
  separatorAfter?: number | number[]
  separator?:      string                    // default: '—'
  masked?:         boolean
  maskChar?:       string                    // default: '●'
}

UseOTPResult

otp is a Svelte store — $otp gives the full OTPStateSnapshot. All other stores must be destructured before subscribing with $:

type UseOTPResult = {
  // Main store — $otp gives OTPStateSnapshot
  subscribe: Writable<OTPStateSnapshot>['subscribe']

  // Derived (read-only) stores — destructure, then use $storeName
  value:        Readable<string>
  isComplete:   Readable<boolean>
  hasError:     Readable<boolean>
  hasSuccess:   Readable<boolean>
  activeSlot:   Readable<number>
  slots:        Readable<SlotEntry[]>
  wrapperAttrs: Readable<Record<string, string | undefined>>

  // Writable stores — destructure, then use $storeName
  timerSeconds:   Writable<number>
  timer:          TimerController
  isDisabled:     Writable<boolean>
  isReadOnly:     Writable<boolean>
  separatorAfter: Writable<number | number[]>
  separator:      Writable<string>
  masked:         Writable<boolean>
  maskChar:       Writable<string>

  placeholder: string   // plain string — no $ needed

  // Svelte action
  action(node: HTMLInputElement): { destroy: () => void }

  // Methods
  getCode():                         string
  getSlots():                        SlotEntry[]    // non-reactive snapshot
  getInputProps(index: number):      InputProps & { 'data-focus': 'true' | 'false' }
  reset():                           void
  resend():                          void
  setError(v: boolean):              void
  setSuccess(v: boolean):            void
  setDisabled(v: boolean):           void
  setReadOnly(v: boolean):           void
  setValue(v: string | undefined):   void   // programmatic fill; no onComplete
  focus(slotIndex: number):          void
}

SlotEntry (from $slots)

type SlotEntry = {
  index:    number
  value:    string    // slot character; '' when unfilled
  isActive: boolean
  isFilled: boolean
}

Compatibility

EnvironmentRequirement
Svelte≥ 4
SvelteKit✅ (SSR-safe)
@verino/coreSame monorepo release
TypeScript≥ 5.0
Node.js (SSR)≥ 18
Module formatESM + CJS

Integration with Core

useOTP calls createOTP() from @verino/core internally. Filtering, cursor logic, paste normalization, and event routing live in core; countdown, feedback, and toolkit helpers come from @verino/core/toolkit. The adapter maps that toolkit behavior into Svelte stores and actions.

See the @verino/core README for the full state machine and event reference.

Contributing

This package lives in the verino monorepo. See CONTRIBUTING.md for guidelines.

# Clone and install
git clone https://github.com/boastack/verino.git
cd verino && pnpm i

# Run before opening a PR
pnpm --filter @verino/svelte build && pnpm test

Ecosystem

PackagePurpose
@verino/coreOTP state machine + toolkit
@verino/vanillaVanilla DOM adapter + timerUIPlugin, webOTPPlugin, pmGuardPlugin
@verino/reactuseOTP hook + HiddenOTPInput component (React ≥ 18)
@verino/vueuseOTP composable with reactive Vue refs (Vue ≥ 3)
@verino/alpineVerinoAlpine plugin — x-verino directive (Alpine.js ≥ 3)
@verino/web-component<verino-input> Shadow DOM custom element

License

MIT © 2026 Olawale Balo

Keywords

otp

FAQs

Package last updated on 21 Jul 2026

Related posts