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 numoraVue 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 torphScript 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:
- Svelte numeric input –
use:numoraaction for Svelte and SvelteKit - Angular numeric input – standalone directive with
ControlValueAccessor - SolidJS numeric input – signal-bound
onMountwrapper - React numeric input – drop-in
<NumoraInput />component (numora-react)