Svelte Numeric Input
Numora is a precision-first Svelte numeric input library: thousand separators, decimal limits, paste sanitisation, scientific-notation expansion and cursor preservation – all driven by a 10-line use:numora action. Svelte's use: actions are designed for libraries that own a DOM node and need cleanup on unmount, which is exactly what NumoraInput does. No numora-svelte wrapper, no peer deps, no SSR special-casing – the core is framework-agnostic vanilla TypeScript, 6.4 kb gzipped, with zero runtime dependencies.
Install
pnpm add numora
# or
npm install numoraSvelte action
NumoraInput attaches directly to an existing <input> element. A Svelte action is the idiomatic way to bind that lifecycle to a DOM node. Drop this five-line helper into your project once and reuse it everywhere.
src/lib/numora.ts:
import { NumoraInput, type NumoraInputOptions } from 'numora';
export function numora(node: HTMLInputElement, options: NumoraInputOptions) {
new NumoraInput(node, options);
// No destroy hook needed - listeners die with the <input> when Svelte unmounts it.
}Component <script lang="ts">:
import { numora } from '$lib/numora';
import { ThousandStyle } from 'numora';
let value = $state('');Component markup:
<label>
Amount
<input
use:numora={{
thousandStyle: ThousandStyle.Thousand,
decimalMaxLength: 2,
onChange: (v) => (value = v),
}}
/>
</label>
<p>Raw value: {value}</p>The action receives the <input> Svelte renders and calls new NumoraInput(node, options) – Numora adopts the element, forces the required attributes (type, inputmode, spellcheck, autocomplete, pattern) and wires its listeners. onChange receives the raw, separator-free string – safe to feed into your store or form library.
Because the <input> is yours, set placeholder, aria-label, name or any other attribute on it directly in the markup.
Animated example: Numora + Torph in Svelte
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 Svelte 5 adapter.
pnpm add numora torphScript block (inside <script lang="ts">):
import { NumoraInput, FormatOn, ThousandStyle, type NumoraInputOptions } from 'numora';
import { TextMorph } from 'torph';
// Action: mounts NumoraInput + TextMorph in the same wrapper and bridges them.
export function numoraTorph(node: HTMLElement, options: NumoraInputOptions) {
const display = node.querySelector<HTMLElement>('.numora-overlay-display')!;
const host = node.querySelector<HTMLElement>('.numora-overlay-host')!;
// SSR placeholder is a text node; Torph only animates element children.
display.textContent = '';
const numora = new NumoraInput(host, {
formatOn: FormatOn.Change,
thousandStyle: ThousandStyle.Thousand,
...options,
});
const input = numora.getElement();
const morph = new TextMorph({
element: display,
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);
return {
destroy: () => {
input.removeEventListener('beforeinput', scheduleSync);
input.removeEventListener('input', syncMorph);
input.remove();
},
};
}Markup:
<label
class="numora-overlay"
use:numoraTorph={{ decimalMaxLength: 2, thousandSeparator: ',' }}
>
<span class="numora-overlay-display" aria-hidden="true">0</span>
<div class="numora-overlay-host" />
</label>Styles (inside the component's <style> block):
.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; }
/* :global(...) escapes Svelte's scoped-class hashing so the rule can reach
the <input> NumoraInput creates at runtime. */
.numora-overlay-host :global(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 :global(input::selection) { background: rgba(255, 255, 255, 0.25); }
.numora-overlay-host :global(input::placeholder) { color: transparent; }The beforeinput microtask + input listener combination guarantees Torph stays in sync across typing, paste, undo and redo. Torph respects prefers-reduced-motion automatically.
FAQ
Is there a numora-svelte package?
No - and there does not need to be. Numora is intentionally a thin layer over the native <input>. A ten-line action is the entire Svelte adapter.
Does Numora work with SvelteKit and SSR?
Yes. NumoraInput touches the DOM, so initialise it inside an action or onMount - both only run in the browser. SvelteKit renders an empty host element on the server, and Numora mounts the formatted <input> on hydration. No SSR-specific configuration is needed.
How do I add thousand separators to a Svelte number input?
Pass thousandStyle: ThousandStyle.Thousand to the use:numora action. Numora formats as the user types, repositions the caret through the inserted comma, and emits the raw separator-free string from onChange - safe to feed straight into a store or form library.
Does it support i18n (decimal commas, currency formatting) in Svelte?
Yes. Set decimalSeparator: ',' and thousandSeparator: '.' (or use the locale option) for European locales. Numora keeps the raw value as a string end-to-end, so currency math stays precise - no parseFloat rounding errors on amounts like 0.1 + 0.2.
Why not use a normal <input> with bind:value?
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 bind:value means re-implementing the 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:
- Vue numeric input – Vue 3
v-numoracustom directive - Angular numeric input – standalone directive with
ControlValueAccessor - SolidJS numeric input – signal-bound
onMountwrapper - React numeric input – drop-in
<NumoraInput />component (numora-react)