npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@daformat/react-number-flow-input

v1.1.1

Published

A react number flow input component, with no dependencies

Readme

React number flow input

NPM Version NPM Downloads
Follow daformat on GitHub Follow daformat on X

A zero-dependency React component that renders an animated number input. Digits animate in as they are typed, selecting and replacing a single digit gives you the popular barrel-wheel effect made famous by NumberFlow, and external value changes animate as a coordinated barrel-wheel roll across every digit.

Demo

https://hello-mat.com/design-engineering/component/number-flow-input

Features

  • Zero runtime dependencies — peer-depends on React >= 18, nothing else.
  • Two synchronized inputs — a contenteditable for the animated display and a hidden <input> for native form integration (name, form, required, ...).
  • Controlled or uncontrolled — use value or defaultValue.
  • Locale-aware formatting — optional Intl.NumberFormat thousand separators and locale decimal characters.
  • Smart editing — undo/redo, copy/cut/paste, decimal-scale clamping, max-length, negative numbers, leading-zero handling, etc.
  • Custom validationisAllowed(value) predicate to reject values you don't like.
  • Animations included — digit flow-in, barrel-wheel digit rolls, separator slide-in/out, width animation on group changes.
  • Minimal styles auto-injected — a <style> tag is added to <head> on first mount, no CSS import required. SSR-safe.
  • Fully typed — ships with TypeScript types.
  • Well tested — 220+ unit and integration tests.

Installation

npm install @daformat/react-number-flow-input
yarn add @daformat/react-number-flow-input
pnpm add @daformat/react-number-flow-input
bun add @daformat/react-number-flow-input
deno add npm:@daformat/react-number-flow-input

Quick start

import { NumberFlowInput } from "@daformat/react-number-flow-input";

export function Example() {
  return (
    <NumberFlowInput
      defaultValue={1234}
      format
      onChange={(value) => console.log(value)}
    />
  );
}

Usage

Uncontrolled

<NumberFlowInput defaultValue={42} onChange={(value) => console.log(value)} />

Controlled

import { useState } from "react";
import { NumberFlowInput } from "@daformat/react-number-flow-input";

function Controlled() {
  const [value, setValue] = useState<number | undefined>(0);
  return <NumberFlowInput value={value} onChange={setValue} />;
}

External updates to value are diffed against the previous value and animate as a coordinated barrel-wheel roll. Initial mount never animates.

To opt out of animations on external value changes — for example when restoring a value programmatically or when binding to a noisy state source — pass animateOnValueChange={false}:

<NumberFlowInput
  value={value}
  onChange={setValue}
  animateOnValueChange={false}
/>

User typing and format / locale toggles still animate; only the prop-driven value updates snap.

Formatted display

<NumberFlowInput format value={1234567} /> // → "1,234,567"
<NumberFlowInput format locale="de-DE" value={1234567} /> // → "1.234.567"

Custom formatter

format also accepts a function for full control over the displayed string. The callback receives the raw display value (digits, optional leading -, optional single . as decimal) and must return the formatted text:

<NumberFlowInput
  defaultValue={1234.5}
  format={(raw) => `$ ${raw}`}
  onChange={console.log}
/>
// → "$ 1234.5"

The callback is only invoked for "real" values — empty / "-" / "." / "-." intermediate states bypass it and render verbatim. If your function throws, the component falls back to a safe locale-decimal-swap output.

For correct cursor positioning and animation diffing, your output should:

  • use the locale's decimal character (or . if no locale is set);
  • preserve the digit order from the raw input.

Decimal scale & negative numbers

<NumberFlowInput allowNegative decimalScale={2} defaultValue={-1234.5} format />

decimalScale={0} prevents the user from typing a decimal point at all. decimalScale={n} clamps the number of fractional digits.

Locale

<NumberFlowInput locale="fr-FR" defaultValue={1234.5} format />
// Renders "1 234,5" (or the locale's group separator).

The component accepts both . and the locale's decimal separator as input — typing either one resolves to the locale's decimal in the display.

Custom validation

<NumberFlowInput
  isAllowed={(value) => value == null || (value >= 0 && value <= 100)}
/>

Any keystroke that would produce a value outside the allowed range is rejected and never reaches onChange.

Length limit

<NumberFlowInput maxLength={6} />

Form integration

The component renders a hidden <input> (offscreen, readonly) that mirrors the current numeric value, so it participates in native form submissions:

<form action="/submit" method="post">
  <NumberFlowInput name="price" required min={0} max={9999} defaultValue={0} />
  <button type="submit">Save</button>
</form>

name, form, required, min, max, minLength and maxLength are forwarded to the hidden input.

Auto focus / events

<NumberFlowInput
  autoFocus
  onFocus={() => console.log("focused")}
  onBlur={() => console.log("blurred")}
/>

Ref

The ref is forwarded to the contenteditable element:

const ref = useRef<HTMLElement>(null);
<NumberFlowInput ref={ref} />;

API

import type {
  NumberFlowInputProps,
  NumberFlowInputCommonProps,
  NumberFlowInputControlledProps,
  NumberFlowInputUncontrolledProps,
} from "@daformat/react-number-flow-input";

Value props

| Prop | Type | Default | Description | | ---------------------- | ------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | value | number \| string \| undefined | — | Controlled value. Accepts a number or a numeric string (see String values). Changes animate as a barrel-wheel roll (except on initial mount). | | defaultValue | number \| string | — | Uncontrolled starting value. Accepts the same shapes as value. | | onChange | (value) => void | — | Called with the parsed number (or undefined for intermediate states like "", "-", ".", "-."). | | onChangeText | (rawText) => void | — | Fires alongside onChange with the raw string representation (e.g. "12345678901234567890.123"). Use this when you need to preserve precision beyond JavaScript's number — see Precision. | | animateOnValueChange | boolean | true | When false, external value updates snap instantly — no digit-roll, no separator slide, no flow animation. Typing and format / locale toggles still animate. |

value and defaultValue are mutually exclusive — TypeScript will enforce this.

Formatting

| Prop | Type | Default | Description | | -------------------- | ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------- | | format | boolean \| (raw: string) => string | false | true → group via Intl.NumberFormat. A function takes full control of the output (see Custom formatter). | | locale | string \| Intl.Locale | — | Locale used for decimal and group separators. Defaults to the runtime's locale. | | decimalScale | number | — | Max number of fractional digits. 0 forbids a decimal point entirely. | | autoAddLeadingZero | boolean | false | Convert leading .50.5 (and -.5-0.5) automatically. | | allowNegative | boolean | false | Allow typing a leading - to enter negative numbers. |

Editing constraints

| Prop | Type | Description | | ----------- | --------------------------------- | -------------------------------------------------------------------------- | | maxLength | number | Maximum raw length the user can type (counted before formatting). | | minLength | number | Forwarded to the hidden <input> for form validation. | | min/max | number | Forwarded to the hidden <input> for form validation. | | isAllowed | (value: number \| null) => bool | Predicate that gates every change. Return false to reject the keystroke. |

DOM / form passthroughs

id, name, form, required, placeholder, className, style, onFocus, onBlur, autoFocus. className and style are applied to the root wrapper <span>.

Styling

Styles are injected globally on first mount. Every selector is scoped to [data-numberflow-input-root], so they won't leak into your app.

The DOM structure (simplified):

<span data-numberflow-input-root class="{className}">
  <span data-numberflow-input-wrapper>
    <span
      role="textbox"
      contenteditable="true"
      data-numberflow-input-contenteditable
      data-placeholder="{placeholder}"
    >
      <span data-char-index="0" data-flow data-show>1</span>
      <span data-char-index="1">,</span>
      <!-- ...one span per character... -->
    </span>
    <input data-numberflow-input-real-input type="string" readonly />
    <!-- barrel-wheel overlays are appended here while animating -->
  </span>
</span>

You can target any of the above data attributes to customize the look:

[data-numberflow-input-contenteditable] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

[data-numberflow-input-contenteditable]:empty::before {
  color: #999; /* placeholder color */
}

Animation timings live in the injected stylesheet and use cubic-bezier(.215, .61, .355, 1) (ease-out-cubic). The flow-in animation is 0.2s; the barrel-wheel roll and width animation are 0.4s.

Precision

The component is built around a string-based internal representation, so what the user types is preserved character-by-character — there's no silent rounding inside the input itself. Where you can run into precision loss is at the boundaries of JavaScript's number type:

| Boundary | Lossy? | Reason | | ------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | User typing → DOM display | No | Every keystroke is applied to a string; the DOM is updated with textContent. | | User typing → onChange(value) | Yes, for > Number.MAX_SAFE_INTEGER or > 15–17 sig. figs. | value is parseFloat(rawText); IEEE 754 double cannot represent every decimal exactly. | | Formatted display, integer part | Yes, for integers > Number.MAX_SAFE_INTEGER | When format is on, the integer part is re-formatted through Intl.NumberFormat.format(parseFloat(rawText)). | | Formatted display, decimal part | No | The decimal part is restored verbatim from the raw string after Intl formatting. | | value prop → display | Inherits the precision of the value the parent already computed. | E.g. 0.1 + 0.2 === 0.30000000000000004 — the component displays exactly what JS gave it. | | defaultValue prop → display | Same as above. | — |

String values

To complete the round-trip for arbitrary-precision use cases, value and defaultValue also accept a numeric string in addition to a number:

// Preserves trailing zeros that a `number` would drop
<NumberFlowInput value="1.50" onChangeText={setRaw} />

// Preserves integers beyond Number.MAX_SAFE_INTEGER
<NumberFlowInput value="12345678901234567890" onChangeText={setRaw} />

// Currency stored as a string
<NumberFlowInput defaultValue="100.00" format />

Strings are sanitized with the same pipeline as user input — only characters matching /^-?\d*\.?\d*$/ survive. Junk strings collapse to an empty value (e.g. "$1,234.56""1234.56", "abc"""). Use . as the decimal separator regardless of locale.

onChange always receives a parsed number (so the JavaScript precision boundary still applies on that side). Pair string props with onChangeText if your parent state must keep full precision.

onChangeText for arbitrary-precision values

When you need the exact digits the user typed (BigInt math, currency stored as strings, big-decimal libraries, etc.), use the onChangeText callback — it fires alongside onChange with the raw string representation:

import { useState } from "react";
import { NumberFlowInput } from "@daformat/react-number-flow-input";

function HugeNumber() {
  const [raw, setRaw] = useState("");

  return (
    <>
      <NumberFlowInput onChangeText={setRaw} />
      <p>BigInt: {raw === "" ? "—" : BigInt(raw.split(".")[0]).toString()}</p>
    </>
  );
}

The string is the unformatted internal representation: digits, an optional leading -, and at most one . (always ., never the locale decimal). Intermediate states like "", "-", ".", "-." are surfaced as-is so consumers can render them if they want.

If you only consume onChange (the typical case), be aware that values past 9.007 × 10¹⁵ (the safe-integer ceiling) or with more than ~17 significant digits will round.

Server-side rendering

injectStyles() is a no-op on the server and idempotent on the client. The component itself only touches the DOM inside useInsertionEffect / useEffect, so it renders cleanly in Next.js, Remix and other SSR frameworks.

Browser support

Modern evergreen browsers. Required browser features:

  • Intl.NumberFormat (for format / locale)
  • Web Animations API (element.animate(...)) — used for the barrel-wheel and position animations
  • CSS transition + transform — used for flow-in animation
  • requestAnimationFrame, ResizeObserver

Development

pnpm install
pnpm test          # vitest run
pnpm build --watch # tsc -p tsconfig.build.json
pnpm format        # prettier --write .
pnpm lint:js       # eslint .

Project layout

src/
├── NumberFlowInput.tsx       # The component
├── styles.ts                 # Injected stylesheet
├── index.ts                  # Public entry point
└── utils/
    ├── barrelWheel.ts        # Wheel DOM helpers
    ├── changes.ts            # Diffing (typing & replacement)
    ├── combineRefs.ts        # Ref forwarding helper
    ├── cssEasing.ts          # Cubic-bezier tokens
    ├── formatting.ts         # Intl.NumberFormat wrapper
    ├── moveElementPreservingAnimation.ts
    ├── textCleaning.ts       # Raw text sanitization
    └── utils.ts              # DOM/measurement helpers

Every util has its own *.test.ts file next to it; component-level tests live in src/NumberFlowInput.test.tsx.

License

Zero-Clause BSD — do whatever you want with it.