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/vue

v2.0.0

Published

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

Readme


Overview

@verino/vue wraps @verino/core in a useOTP composable. All state is exposed as Vue Ref values so templates react automatically to slot changes, timer ticks, and field state.

Event handlers are returned as named functions you bind to the hidden <input> with @keydown, @input, @paste, @focus, and @blur. Visual slot divs are purely decorative mirrors and hold no event listeners.

Use value for live external control with a Vue ref, computed, or getter. Use defaultValue for one-time prefill on mount.


Why Use This Adapter?

  • First-class Vue 3 reactivity. All state is exposed as Ref<T> — no manual syncing required.
  • Live external control. Pass a Vue watch source for seamless two-way binding.
  • Full template control. No opaque component — you own the template and choose the markup.
  • Composition API native. Works seamlessly with <script setup> and the Options API.

Installation

# npm
npm i @verino/vue

# pnpm
pnpm add @verino/vue

# yarn
yarn add @verino/vue

Peer dependency: Vue ≥ 3. @verino/core installs automatically.


Quick Start

<script setup lang="ts">
import { useOTP } from '@verino/vue'

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

<template>
  <div
    v-bind="otp.wrapperAttrs.value"
    style="position: relative; display: inline-flex; gap: 8px"
  >
    <input
      :ref="(el) => (otp.inputRef.value = el as HTMLInputElement)"
      v-bind="otp.hiddenInputAttrs.value"
      style="position: absolute; inset: 0; opacity: 0; z-index: 1; cursor: text"
      @keydown="otp.onKeydown"
      @input="otp.onChange"
      @paste="otp.onPaste"
      @focus="otp.onFocus"
      @blur="otp.onBlur"
    />

    <div
      v-for="slot in otp.getSlots()"
      :key="slot.index"
      aria-hidden="true"
      :class="[
        'slot',
        slot.isActive && otp.isFocused.value && 'is-active',
        slot.isFilled && 'is-filled',
        otp.hasError.value && 'is-error',
      ].filter(Boolean)"
    >
      <span v-if="slot.isActive && !slot.isFilled && otp.isFocused.value" class="caret" />
      {{ slot.isFilled ? slot.value : otp.placeholder }}
    </div>
  </div>
</template>

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 Vue, live external control means passing a watch source that the composable can observe through Vue's reactivity system. Changes propagate automatically without triggering onComplete:

const code = ref('')
const otp  = useOTP({ length: 6, value: code })

// Clearing from parent:
code.value = ''

To propagate user keystrokes back to the ref, add onChange:

const otp = useOTP({ length: 6, value: code, onChange: (v) => (code.value = v) })

Async verification

Set otp.isDisabled.value directly to lock the field during an async call:

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

Timer

timerSeconds is a live reactive ref — it updates every second while the timer is running:

<script setup lang="ts">
const otp = useOTP({ length: 6, timer: 60, onExpire: () => showExpiredMessage() })
</script>

<template>
  <p v-if="otp.timerSeconds.value > 0">
    Expires in {{ Math.floor(otp.timerSeconds.value / 60) }}:{{ String(otp.timerSeconds.value % 60).padStart(2, '0') }}
  </p>
</template>

Separator

separatorAfter is a reactive ref. Use computed to build a Set that handles both number and number[]:

<script setup lang="ts">
import { computed } from 'vue'

const otp    = useOTP({ length: 6, separatorAfter: 3, separator: '—' })
const sepSet = computed(() => {
  const v = otp.separatorAfter.value
  return new Set(Array.isArray(v) ? v : [v])
})
</script>

<template>
  <template v-for="slot in otp.getSlots()" :key="slot.index">
    <span v-if="sepSet.has(slot.index)" aria-hidden="true">{{ otp.separator.value }}</span>
    <div aria-hidden="true" class="slot">{{ slot.value }}</div>
  </template>
</template>

Masked input

<template>
  <div
    v-for="slot in otp.getSlots()"
    :key="slot.index"
    aria-hidden="true"
    class="slot"
  >
    {{ slot.isFilled ? (otp.masked.value ? otp.maskChar.value : slot.value) : otp.placeholder }}
  </div>
</template>

data-* state attributes

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

<script setup lang="ts">
function slotDataAttrs(index: number) {
  const props = otp.getInputProps(index)
  const out: Record<string, unknown> = {}
  for (const key in props) if (key.startsWith('data-')) out[key] = props[key]
  return out
}
</script>

<template>
  <div
    v-for="slot in otp.getSlots()"
    :key="slot.index"
    class="slot"
    aria-hidden="true"
    v-bind="slotDataAttrs(slot.index)"
  >
    {{ slot.value }}
  </div>
</template>

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.value on the container for wrapper-level attributes:

<div v-bind="otp.wrapperAttrs.value">
  <!-- 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?: VueOTPOptions): UseOTPResult

VueOTPOptions

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

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

UseOTPResult

type UseOTPResult = {
  // Reactive refs — access as otp.xxx.value in script, otp.xxx in template
  slotValues:       Ref<string[]>
  activeSlot:       Ref<number>
  value:            Ref<string>            // computed joined code
  isComplete:       Ref<boolean>
  hasError:         Ref<boolean>
  hasSuccess:       Ref<boolean>
  isDisabled:       Ref<boolean>           // set directly to lock/unlock
  isFocused:        Ref<boolean>
  timerSeconds:     Ref<number>            // live countdown; 0 when expired or no timer
  separatorAfter:   Ref<number | number[]>
  separator:        Ref<string>
  masked:           Ref<boolean>
  maskChar:         Ref<string>
  placeholder:      string                 // plain string — no .value needed

  // Bindings
  inputRef:         Ref<HTMLInputElement | null>
  hiddenInputAttrs: Ref<Record<string, unknown>>
  wrapperAttrs:     Ref<Record<string, string | undefined>>

  // Event handlers (bind to hidden input)
  onKeydown(e: KeyboardEvent): void
  onChange(e: Event):          void
  onPaste(e: ClipboardEvent):  void
  onFocus():                   void
  onBlur():                    void

  // Methods
  getCode():                         string
  getSlots():                        SlotEntry[]
  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
  focus(slotIndex: number):          void
}

SlotEntry

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

Compatibility

| Environment | Requirement | |---|---| | Vue | ≥ 3.3 | | @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 composable maps that toolkit behavior into Vue refs and event handlers.

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/vue 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/svelte | useOTP store + use:action directive (Svelte ≥ 4) | | @verino/alpine | VerinoAlpine plugin — x-verino directive (Alpine.js ≥ 3) | | @verino/web-component | <verino-input> Shadow DOM custom element |


License

MIT © 2026 Olawale Balo