XGitHub

Vue Numeric Input

Numora is a precision-first Vue numeric input library: thousand separators, decimal limits, paste sanitisation, scientific-notation expansion and cursor preservation – all driven by a six-line v-numora custom directive that mounts directly on an <input>. The core is framework-agnostic vanilla TypeScript, 6.4 kb gzipped, with zero runtime dependencies – the same engine works inside Vue 3, Nuxt 3, VeeValidate, FormKit and Pinia without any wrapper package.

Install

pnpm add numora
# or
npm install numora

Vue 3 directive

NumoraInput attaches directly to an existing <input> element. A custom directive is the idiomatic Vue 3 adapter – drop this helper into your project once and every <input v-numora="opts"> works.

src/directives/numora.ts:

import type { Directive } from 'vue';
import { NumoraInput, type NumoraInputOptions } from 'numora';

export const vNumora: Directive<HTMLInputElement, NumoraInputOptions> = {
  mounted(el, binding) {
    new NumoraInput(el, binding.value ?? {});
    // No unmounted hook needed - listeners die with the <input> when Vue removes it.
  },
};

Register globally in src/main.ts (or import + declare locally in any <script setup>):

import { createApp } from 'vue';
import App from './App.vue';
import { vNumora } from './directives/numora';

createApp(App).directive('numora', vNumora).mount('#app');

Component <script setup lang="ts">:

import { ref } from 'vue';
import { ThousandStyle } from 'numora';

const value = ref('');

Component <template>:

<label>
  Amount
  <input
    v-numora="{
      thousandStyle: ThousandStyle.Thousand,
      decimalMaxLength: 2,
      onChange: (v) => (value = v),
    }"
  />
</label>
<p>Raw value: {{ value }}</p>

The directive receives the <input> Vue renders and calls new NumoraInput(el, binding.value). Numora adopts the element, forces the required attributes (type, inputmode, spellcheck, autocomplete, pattern), and emits the raw, separator-free string from onChange – safe to feed straight into a ref, Pinia store, or VeeValidate field.

Because the <input> is yours, set placeholder, aria-label, name or any other attribute on it directly in the template.

Animated example: Numora + Torph in Vue 3

For animated digit transitions, stack a Torph TextMorph on top of a transparent-text NumoraInput. The input owns the keyboard, undo and IME; Torph animates the visible characters. The vanilla overlay guide covers the pattern in depth - below is the idiomatic Vue 3 adapter.

pnpm add numora torph

Script block (inside <script setup lang="ts">):

import { onBeforeUnmount, onMounted, ref } from 'vue';
import { NumoraInput, FormatOn, ThousandStyle } from 'numora';
import { TextMorph } from 'torph';

const display = ref<HTMLSpanElement | null>(null);
const host = ref<HTMLDivElement | null>(null);

let cleanup: (() => void) | null = null;

onMounted(() => {
  if (!display.value || !host.value) return;

  // SSR placeholder is a text node; Torph only animates element children.
  display.value.textContent = '';

  const numora = new NumoraInput(host.value, {
    formatOn: FormatOn.Change,
    thousandStyle: ThousandStyle.Thousand,
    decimalMaxLength: 2,
    thousandSeparator: ',',
  });
  const input = numora.getElement();

  const morph = new TextMorph({
    element: display.value,
    ease: { stiffness: 400, damping: 30 },
  });
  morph.update('0');

  const syncMorph = () => morph.update(numora.value || '0');
  // Numora writes the formatted value in beforeinput; sync after that handler runs.
  const scheduleSync = () => queueMicrotask(syncMorph);

  input.addEventListener('beforeinput', scheduleSync);
  input.addEventListener('input', syncMorph);

  cleanup = () => {
    input.removeEventListener('beforeinput', scheduleSync);
    input.removeEventListener('input', syncMorph);
  };
});

onBeforeUnmount(() => cleanup?.());

Template (inside <template>):

<label class="numora-overlay">
  <span ref="display" class="numora-overlay-display" aria-hidden="true">0</span>
  <div ref="host" class="numora-overlay-host" />
</label>

Styles (inside <style scoped>):

.numora-overlay {
  position: relative;
  display: inline-flex;
  align-items: center;
  font: 2.25rem/1 ui-monospace, SFMono-Regular, monospace;
  color: white;
}
.numora-overlay-display { pointer-events: none; white-space: pre; }
.numora-overlay-host { position: absolute; inset: 0; }

/* :deep(...) escapes Vue's scoped-style attribute so the rule can reach
   the <input> NumoraInput creates at runtime. */
.numora-overlay-host :deep(input) {
  width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0;
  background: transparent; color: transparent; caret-color: white;
  outline: none; font: inherit;
}
.numora-overlay-host :deep(input::selection) { background: rgba(255, 255, 255, 0.25); }
.numora-overlay-host :deep(input::placeholder) { color: transparent; }

The beforeinput microtask + input listener combination keeps Torph in sync across typing, paste, undo and redo. Torph respects prefers-reduced-motion automatically.

FAQ

Is there a numora-vue package?

No - and there does not need to be. Numora is intentionally a thin layer over the native <input>. A six-line v-numora directive is the entire Vue adapter.

Does Numora work with Nuxt 3 and SSR?

Yes. NumoraInput touches the DOM, so the v-numora directive only initialises in the mounted hook – that path only runs in the browser. Nuxt renders the empty <input> on the server and Numora adopts it on hydration. No Nuxt-specific configuration needed.

How do I add thousand separators to a Vue number input?

Pass thousandStyle: ThousandStyle.Thousand inside the v-numora binding. Numora formats as the user types, keeps the caret stable through the inserted comma, and emits the raw separator-free string from onChange – safe to feed into ref, Pinia, VeeValidate or FormKit.

Does it support i18n (decimal commas, currency formatting) in Vue?

Yes. Set decimalSeparator: ',' and thousandSeparator: '.' (or use the locale option) for European formats. Numora keeps the raw value as a string end-to-end, so currency math stays precise – no parseFloat rounding errors.

Why not v-model on a plain <input>?

Numora handles thousand separators, decimal limits, scientific notation, paste sanitisation, cursor preservation through formatting, and mobile keyboard hints (inputmode="decimal"). Doing all of that on a plain v-model means re-implementing cursor-positioning logic by hand - that's the part Numora exists to solve.

Numora in other frameworks

Numora's core is framework-agnostic. The same vanilla NumoraInput class powers the numeric input across every modern UI framework: