Torph Library Integration with Numora React
Torph by Lochie Axon is an animated text-morphing library. This guide layers a Torph TextMorph on top of a transparent-text Numora React NumoraInput so the editable surface itself appears animated. The real <input> still handles every keystroke; Torph just renders what the user sees.
Using vanilla JS? See the core Numora Torph integration - it uses NumoraInput and TextMorph directly with a DOM event bridge.
FormatOn.Change - separators animate on every keystroke.
FormatOn.Blur - minimal integration; the caret floats during the focus-strip morph.
How the overlay works
Native <input> elements render their value as a string with no child DOM, so animation libraries can't inject animated spans into them directly. The overlay sidesteps that constraint by stacking two layers in the same box:
- Visible layer: a
<TextMorph>that animates the formatted display string. - Keyboard layer: the real
NumoraInputpositioned on top withcolor: transparentand a visible caret. It still owns focus, keystrokes, selection, undo, IME, and mobileinputmode.
Both layers render the same formatted string. As the user types, numora's onChange fires; the formatted value flows into the TextMorph; Torph diffs the old and new strings and animates each digit / separator into place. The input itself never animates - but because its text is transparent, you only see the Torph layer.
Installation
pnpm add numora-react torph
# or
npm install numora-react torphBuilding the overlay
Implementation is short. The key constraints: identical typography on both layers, FormatOn.Change so the input's text matches the morph's text on every keystroke, and zero padding/border on the input so its text origin lines up with the overlay span.
import { FormatOn } from 'numora'
import { NumoraInput, type NumoraInputChangeEvent } from 'numora-react'
import { useState } from 'react'
import { TextMorph } from 'torph/react'
function AnimatedInput() {
const [value, setValue] = useState('')
const [formatted, setFormatted] = useState('')
return (
<label className="relative inline-flex items-center text-4xl font-mono leading-none text-white">
{/* Visible layer: Torph renders the animated text */}
<span aria-hidden="true" className="pointer-events-none whitespace-pre">
<TextMorph ease={{ stiffness: 400, damping: 30 }}>
{formatted || '0'}
</TextMorph>
</span>
{/* Invisible layer: real input owns the keyboard */}
<NumoraInput
value={value}
onChange={(e: NumoraInputChangeEvent) => {
setValue(e.target.value)
setFormatted(e.target.formattedValue || '')
}}
formatOn={FormatOn.Change}
decimalMaxLength={2}
thousandSeparator=","
aria-label="Amount"
className="absolute inset-0 w-full h-full m-0 p-0 border-0 bg-transparent
text-transparent placeholder-transparent caret-white
outline-none focus:outline-none
text-4xl font-mono leading-none"
/>
</label>
)
}Why each line matters
formatOn: FormatOn.Change(used here) - keeps the input's display as the formatted string at all times, so passingformattedValueto Torph on everyonChangeis enough.FormatOn.Bluralso works, but the focus event silently strips separators (noonChangefires for it) - to use Blur mode addonFocus={(e) => setFormatted(e.target.value)}so Torph stays in sync.- Match typography - share font size, family, leading on both layers and use
m-0 p-0 border-0on the input. The caret is computed from the input's invisible text layout, so any difference in font metrics or text origin shows up as caret drift.
Reducing motion
Torph respects prefers-reduced-motion by default. If a user opts out of animations at the OS level the morph becomes an instant swap - no extra code needed.
Key points
- No precision boundary. Torph operates on strings; numora hands you the formatted string. Pipe it through without ever calling
Number(). - The input still owns the keyboard. Undo, redo, IME, paste, mobile
inputmode="decimal", native form submission all keep working. The overlay is a display layer only. - Pass
formattedValueto Torph. That value always equals the input's current display string. WithFormatOn.Changeno extra wiring is needed. WithFormatOn.Blur, add anonFocushandler to mirror the silent focus-strip - the blur reformat itself firesonChangeand needs nothing. - Match typography. Font family, size, line-height, letter-spacing, and zero padding/border on the input - the caret is computed from the input's text layout, so any difference shows up as caret drift.
FAQ
What is the Torph library?
Torph is an animated text-morphing library by Lochie Axon. It diffs an old string and a new string and animates each character between them. Torph ships a React component at torph/react and a vanilla TextMorph class - this page uses the React component.
How do I use Torph with React?
Import TextMorph from torph/react and render it as the visible layer of a transparent-text NumoraInput overlay. Pass formattedValue from Numora's onChange as the TextMorph child and the digits animate on every keystroke.
Does Torph respect prefers-reduced-motion?
Yes. Torph respects the OS-level prefers-reduced-motion setting by default. If the user opts out of animations, TextMorph becomes an instant swap - no extra code needed.
Where can I install the Torph library?
The Torph library lives at torph.lochie.me and is published on npm as torph. Install both packages with npm install numora-react torph.