npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@verino/svelte

v2.0.0

Published

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

Readme


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 case | Key options | |---|---| | SMS / email OTP | type: 'numeric', timer: 60, onResend | | TOTP / 2FA with separator | separatorAfter: 3 | | PIN entry | masked: true, blurOnComplete: true | | Alphanumeric code | type: 'alphanumeric', pasteTransformer | | Invite / referral code | separatorAfter: [3, 6], pattern: /^[A-Z0-9]$/ | | Hex activation key | pattern: /^[0-9A-F]$/ | | Async verification lock | setDisabled(true / false) around the API call | | Native form submission | name: 'otp_code' | | Pre-fill on mount | defaultValue: '123456' | | Display-only / read-only | readOnly: 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"):

| Attribute | Meaning | |---|---| | data-active | Logical cursor is at this slot (set even when the field is blurred) | | data-focus | Browser focus is on the hidden input | | data-filled | Slot contains a character | | data-empty | Slot is unfilled (complement of data-filled) | | data-invalid | Error state is active | | data-success | Success state is active (mutually exclusive with data-invalid) | | data-disabled | Field is disabled | | data-readonly | Field is in read-only mode | | data-complete | All slots are filled | | data-first | This is the first slot 0 | | data-last | This is the last slot | | data-slot | Zero-based position of the slot as a string ("0", "1", …) |

Wrapper attributes

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

| Attribute | When present | |---|---| | data-complete | All slots are filled | | data-invalid | Error state is active | | data-success | Success state is active | | data-disabled | Field is disabled | | data-readonly | Field is read-only |

/* Slot-level — scope to your field with an id or class prefix */
.slot[data-active="true"][data-focus="true"] { border-color: #3D3D3D; }
.slot[data-filled="true"]                    { background:   #FFFFFF; }
.slot[data-empty="true"]                     { background:   #FAFAFA; }
.slot[data-invalid="true"]                   { border-color: #FB2C36; }
.slot[data-success="true"]                   { border-color: #00C950; }
.slot[data-disabled="true"]                  { opacity: 0.45; pointer-events: none; }
.slot[data-readonly="true"]                  { cursor: default; }
.slot[data-complete="true"]                  { border-color: #00C950; }

/* 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:         #0A0A0A;
  --verino-border-color:  #E5E5E5;
  --verino-active-color:  #3D3D3D;
  --verino-error-color:   #FB2C36;
  --verino-success-color: #00C950;
  --verino-caret-color:   #3D3D3D;

  /* Placeholder, separator & mask */
  --verino-placeholder-color: #D3D3D3;
  --verino-placeholder-size:  16px;
  --verino-separator-color:   #A1A1A1;
  --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-interferencespellcheck="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>
  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

| Environment | Requirement | |---|---| | Svelte | ≥ 4 | | SvelteKit | ✅ (SSR-safe) | | @verino/core | Same monorepo release | | TypeScript | ≥ 5.0 | | Node.js (SSR) | ≥ 18 | | Module format | ESM + 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

| Package | Purpose | |---|---| | @verino/core | OTP state machine + toolkit | | @verino/vanilla | Vanilla DOM adapter + timerUIPlugin, webOTPPlugin, pmGuardPlugin | | @verino/react | useOTP hook + HiddenOTPInput component (React ≥ 18) | | @verino/vue | useOTP composable with reactive Vue refs (Vue ≥ 3) | | @verino/alpine | VerinoAlpine plugin — x-verino directive (Alpine.js ≥ 3) | | @verino/web-component | <verino-input> Shadow DOM custom element |


License

MIT © 2026 Olawale Balo