@daformat/react-number-flow-input

中文文档
1.1.2发布于 2026/09/18Zero-Clause BSDTypeScriptES Module零依赖近 30 天下载 9837

A react number flow input component, with no dependencies

原始使用文档
文档目录29 节
  1. Demo
  2. Features
  3. Installation
  4. Quick start
  5. Usage
  6. Uncontrolled
  7. Controlled
  8. Formatted display
  9. Decimal scale & negative numbers
  10. Locale
  11. Custom validation
  12. Length limit
  13. Form integration
  14. Auto focus / events
  15. Ref
  16. API
  17. Value props
  18. Formatting
  19. Editing constraints
  20. DOM / form passthroughs
  21. Styling
  22. Precision
  23. String values
  24. onChangeText for arbitrary-precision values
  25. Server-side rendering
  26. Browser support
  27. Development
  28. Project layout
  29. License

React number flow input

NPM Version gzipped 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 validation — isAllowed(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

PropTypeDefaultDescription
valuenumber | 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).
defaultValuenumber | 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.
animateOnValueChangebooleantrueWhen 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

PropTypeDefaultDescription
formatboolean | (raw: string) => stringfalsetrue → group via Intl.NumberFormat. A function takes full control of the output (see Custom formatter).
localestring | Intl.Locale—Locale used for decimal and group separators. Defaults to the runtime's locale.
decimalScalenumber—Max number of fractional digits. 0 forbids a decimal point entirely.
autoAddLeadingZerobooleanfalseConvert leading .5 → 0.5 (and -.5 → -0.5) automatically.
allowNegativebooleanfalseAllow typing a leading - to enter negative numbers.

Editing constraints

PropTypeDescription
maxLengthnumberMaximum raw length the user can type (counted before formatting).
minLengthnumberForwarded to the hidden <input> for form validation.
min/maxnumberForwarded to the hidden <input> for form validation.
isAllowed(value: number | null) => boolPredicate 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="text" 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:

BoundaryLossy?Reason
User typing → DOM displayNoEvery 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 partYes, for integers > Number.MAX_SAFE_INTEGERWhen format is on, the integer part is re-formatted through Intl.NumberFormat.format(parseFloat(rawText)).
Formatted display, decimal partNoThe decimal part is restored verbatim from the raw string after Intl formatting.
value prop → displayInherits 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 → displaySame 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.

版本与发布记录

@daformat/react-number-flow-input 共发布 17 个版本,首次发布于 2026/05/17;过去 12 个月发布了 17 个版本。

版本发布日期距今
1.1.2latest2026/09/181 个月前
1.1.12026/05/235 个月前
1.1.02026/05/235 个月前
1.0.132026/05/235 个月前
1.0.122026/05/195 个月前
1.0.112026/05/195 个月前

关于 @daformat/react-number-flow-input 的常见问题

@daformat/react-number-flow-input 是什么?

A react number flow input component, with no dependencies

如何安装 @daformat/react-number-flow-input?

在项目目录运行 npm install @daformat/react-number-flow-input,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@daformat/react-number-flow-input 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@daformat/react-number-flow-input 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@daformat/react-number-flow-input 可以商用吗?

@daformat/react-number-flow-input 使用 Zero-Clause BSD 许可。声明的许可为 Zero-Clause BSD,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@daformat/react-number-flow-input 的下载量有多少?

根据 npm 官方下载统计,@daformat/react-number-flow-input 在 2026/10/11 – 2026/11/09 这 30 天内被下载 9,837 次,日均约 328 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@daformat/react-number-flow-input 还在维护吗?

最新版本 1.1.2 发布于 2026/09/18(1 个月前);至今共发布 17 个版本,其中过去 12 个月发布了 17 个;首次发布于 2026/05/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。