Solid Numeric Input
Numora is a precision-first SolidJS numeric input library: thousand separators, decimal limits, paste sanitisation, scientific-notation expansion and cursor preservation – driven by a four-line use:numora directive that mounts directly on an <input>. Write the raw value into a createSignal setter and Solid's fine-grained reactivity handles the rest. No numora-solid wrapper, no peer deps – the core is framework-agnostic vanilla TypeScript, 6.4 kb gzipped, with zero runtime dependencies.
Install
pnpm add numora
# or
npm install numoraSolid directive
NumoraInput attaches directly to an existing <input> element. Solid's use: directives are the idiomatic way to bind that lifecycle to a DOM node – drop this helper into your project once and every <input use:numora={opts}> works.
src/directives/numora.ts:
import { NumoraInput, type NumoraInputOptions } from 'numora';
export function numora(el: HTMLInputElement, accessor: () => NumoraInputOptions) {
new NumoraInput(el, accessor() ?? {});
// No cleanup needed - listeners die with the <input> when Solid removes it.
}
// Tells Solid's JSX compiler that use:numora is a valid directive.
declare module 'solid-js' {
namespace JSX {
interface Directives {
numora: NumoraInputOptions;
}
}
}Component:
import { createSignal } from 'solid-js';
import { ThousandStyle } from 'numora';
import { numora } from './directives/numora';
// Solid's compiler must see the 'numora' import to keep it - the directive
// looks unused otherwise and tree-shaking would drop it.
void numora;
export function AmountInput() {
const [value, setValue] = createSignal('');
return (
<label>
Amount
<input
use:numora={{
thousandStyle: ThousandStyle.Thousand,
decimalMaxLength: 2,
onChange: (v) => setValue(v),
}}
/>
<p>Raw value: {value()}</p>
</label>
);
}The directive receives the <input> Solid renders and an accessor for its options, then calls new NumoraInput(el, accessor()). Numora adopts the element, forces the required attributes (type, inputmode, spellcheck, autocomplete, pattern), and emits the raw, separator-free string from onChange – write it straight into a signal or store.
Because the <input> is yours, set placeholder, aria-label, name or any other attribute on it directly in the JSX.
Animated example: Numora + Torph in SolidJS
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 SolidJS adapter.
pnpm add numora torphimport { onCleanup, onMount } from 'solid-js';
import { NumoraInput, FormatOn, ThousandStyle } from 'numora';
import { TextMorph } from 'torph';
import './numora-overlay.css';
export function AnimatedAmountInput() {
let display!: HTMLSpanElement;
let host!: HTMLDivElement;
onMount(() => {
// SSR placeholder is a text node; Torph only animates element children.
display.textContent = '';
const numora = new NumoraInput(host, {
formatOn: FormatOn.Change,
thousandStyle: ThousandStyle.Thousand,
decimalMaxLength: 2,
thousandSeparator: ',',
});
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);
onCleanup(() => {
input.removeEventListener('beforeinput', scheduleSync);
input.removeEventListener('input', syncMorph);
});
});
return (
<label class="numora-overlay">
<span ref={display} class="numora-overlay-display" aria-hidden="true">0</span>
<div ref={host} class="numora-overlay-host" />
</label>
);
}/* numora-overlay.css */
.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; }
.numora-overlay-host 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 input::selection { background: rgba(255, 255, 255, 0.25); }
.numora-overlay-host input::placeholder { color: transparent; }Solid doesn't scope CSS by default, so the input selectors work as-is - Numora creates the <input> at runtime inside the host <div>. 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-solid package?
No. Numora is intentionally a thin layer over the native <input>. A four-line use:numora directive is the entire Solid adapter.
Does Numora work with SolidStart and SSR?
Yes. The use:numora directive only runs on the client – SolidStart renders the empty <input> on the server and Numora adopts it on hydration. No SolidStart-specific configuration needed.
How do I add thousand separators to a Solid number input?
Pass thousandStyle: ThousandStyle.Thousand inside the use: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 – write it straight into your createSignal setter.
Does it support i18n (decimal commas, currency formatting) in Solid?
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 a plain signal-bound <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 signal-bound input 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:
- Svelte numeric input –
use:numoraaction for Svelte and SvelteKit - Vue numeric input – Vue 3
v-numoracustom directive - Angular numeric input – standalone directive with
ControlValueAccessor - React numeric input – drop-in
<NumoraInput />component (numora-react)