WebberUI

Chinese Amount Input (React)

A React money input that mirrors Arabic digits into formal Chinese uppercase (新臺幣壹萬貳仟元整) as you type: thousands grouping, jiao/fen handling, one-click copy, and a standalone toChineseUpper() converter for checks, receipts, contracts and quotes.

Checks, receipts, contracts and quotations all need the amount written out in formal Chinese uppercase numerals. This component pairs a numeric input (inputMode="decimal", digits and a single decimal point only, at most two decimals, thousands separators inserted as you type) with a mirror row underneath: every keystroke updates 「新臺幣壹萬貳仟元伍角整」 character by character — only the characters that changed animate, while the prefix and higher digits stay put — and a copy button on the right copies the full string and flashes a check on success. The conversion rules live in a standalone pure function, toChineseUpper(): digits 零壹貳參肆伍陸柒捌玖, places 拾佰仟, groups 萬億兆, decimals 角分, consecutive zeros collapsed, trailing zeros in a group dropped, 「壹拾」 kept, supported up to 兆 (10^12).

Loading preview…
npx shadcn@latest add https://webberui.com/r/chinese-amount-input.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<ChineseAmountInput />

Installation

npx shadcn@latest add https://webberui.com/r/chinese-amount-input.json

Or, once registries are configured in components.json, install it as @webberui/chinese-amount-input.

Usage

import {
  ChineseAmountInput,
  toChineseUpper,
} from "@/components/ui/chinese-amount-input";

<ChineseAmountInput
  label="報價總額(含稅)"
  prefix="新臺幣"
  onChange={(raw, { upper, formatted, amount }) => {
    console.log(raw); // "12000.5"
    console.log(formatted); // "12,000.5"
    console.log(upper); // "新臺幣壹萬貳仟元伍角整"
    console.log(amount); // 12000.5
  }}
/>

// The converter can be used on its own (e.g. when printing a check or generating a PDF)
toChineseUpper(120000.5); // "壹拾貳萬元伍角整"
toChineseUpper("100000001", { prefix: "人民幣" }); // "人民幣壹億零壹元整"
toChineseUpper("1,680.32"); // "壹仟陸佰捌拾元參角貳分" (thousands separators are stripped)

Props

PropTypeDefaultDescription
valuestring | numberControlled value: a plain numeric string (e.g. "12000.5") or a number; when omitted the component is uncontrolled
defaultValuestring | number""Initial value in uncontrolled mode
onChange(raw: string, meta: ChineseAmountChangeDetail) => voidCallback when the value changes: the first argument is the cleaned numeric string, the second carries the uppercase text, the formatted string and the numeric value
prefixstring"新臺幣"Currency prefix for the uppercase text (「新臺幣」 New Taiwan dollar, e.g. 「人民幣」 renminbi); pass an empty string for none
allowDecimalbooleantrueWhether decimals (jiao/fen) are allowed; false accepts integers only
maxnumberUpper limit; exceeding it shows an error message and sets aria-invalid (input is not blocked)
labelstring"金額"Field label text (「金額」 amount)
placeholderstring"0"Input placeholder
disabledbooleanfalseDisable the input
namestringForm field name forwarded to the native input
classNamestringForwarded to the outermost container

ChineseAmountChangeDetail

FieldTypeDefaultDescription
upperstringFull uppercase text including the prefix; "" when the amount exceeds 兆
formattedstringDisplay string with thousands separators
amountnumberParsed numeric value; 0 when empty

toChineseUpper(amount, opts?)

toChineseUpper(amount: number | string, opts?: ToChineseUpperOptions): string — accepts a number or a string (strings may contain thousands separators and full-width digits). Returns "" for an empty or non-numeric input; throws a RangeError for negative amounts or when the integer part exceeds 16 digits (兆). Numbers are rounded to fen (two decimals); strings with more than two decimals are truncated, matching the input's behavior.

FieldTypeDefaultDescription
prefixstring""Currency prefix such as 「新臺幣」 or 「人民幣」
allowDecimalbooleantrueWhether to handle jiao/fen; false drops the fractional part

sanitizeAmount(raw, allowDecimal?) (cleans to a plain numeric string), formatAmount(raw) (adds thousands separators) and the constant MAX_AMOUNT_INT_DIGITS (16) are also named exports.

How it works

Conversion reference (without prefix):

InputOutput
0零元整
10壹拾元整
100壹佰元整
1005壹仟零伍元整
10500壹萬零伍佰元整
120000.5壹拾貳萬元伍角整
1000000壹佰萬元整
100000001壹億零壹元整
1.05壹元零伍分
1680.32壹仟陸佰捌拾元參角貳分
  • 「壹拾」 is never shortened to 「拾」 (12 → 壹拾貳): uppercase amounts exist to resist tampering, unlike the spoken 「十二」
  • Jiao without fen is written 「元X角整」, fen without jiao 「元零X分」, and both 「元X角X分」
  • The integer part supports up to 16 digits (just under 10,000 兆); beyond that the input shows an out-of-range error and sets aria-invalid. When max is set, exceeding it shows a similar message but does not block input, so the user can correct it themselves
  • Full-width digits and the full-width period (a common slip under Chinese IMEs) are accepted and normalized to ASCII; thousands separators in pasted text are stripped and re-inserted for display
  • The caret stays on the same digit after separators are inserted instead of jumping to the end

Accessibility

  • The real input is a single native input (inputMode="decimal"), so mobile devices bring up the numeric keypad; aria-describedby points at both the uppercase mirror row and the hint message
  • The mirror row is an aria-live="polite" region; the per-character animation is aria-hidden, so screen readers only hear the complete string (e.g. 「新臺幣壹萬貳仟元整」)
  • aria-invalid is set when the amount is out of range or above max, and the error message lives in a role="status" region
  • The copy button is type="button" with an aria-label (which switches to "copied" after success), keyboard operable with a focus-visible ring
  • When the user has "reduce motion" enabled at the system level, the uppercase text is swapped in place with no per-character fade or slide, and the copy-icon and message transitions are disabled as well

On this page