XGitHub

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 numora

Svelte 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 torph

Script 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: