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).
npx shadcn@latest add https://webberui.com/r/chinese-amount-input.jsonPlayground
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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | number | — | Controlled value: a plain numeric string (e.g. "12000.5") or a number; when omitted the component is uncontrolled |
defaultValue | string | number | "" | Initial value in uncontrolled mode |
onChange | (raw: string, meta: ChineseAmountChangeDetail) => void | — | Callback 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 |
prefix | string | "新臺幣" | Currency prefix for the uppercase text (「新臺幣」 New Taiwan dollar, e.g. 「人民幣」 renminbi); pass an empty string for none |
allowDecimal | boolean | true | Whether decimals (jiao/fen) are allowed; false accepts integers only |
max | number | — | Upper limit; exceeding it shows an error message and sets aria-invalid (input is not blocked) |
label | string | "金額" | Field label text (「金額」 amount) |
placeholder | string | "0" | Input placeholder |
disabled | boolean | false | Disable the input |
name | string | — | Form field name forwarded to the native input |
className | string | — | Forwarded to the outermost container |
ChineseAmountChangeDetail
| Field | Type | Default | Description |
|---|---|---|---|
upper | string | — | Full uppercase text including the prefix; "" when the amount exceeds 兆 |
formatted | string | — | Display string with thousands separators |
amount | number | — | Parsed 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.
| Field | Type | Default | Description |
|---|---|---|---|
prefix | string | "" | Currency prefix such as 「新臺幣」 or 「人民幣」 |
allowDecimal | boolean | true | Whether 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):
| Input | Output |
|---|---|
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. Whenmaxis 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-describedbypoints at both the uppercase mirror row and the hint message - The mirror row is an
aria-live="polite"region; the per-character animation isaria-hidden, so screen readers only hear the complete string (e.g. 「新臺幣壹萬貳仟元整」) aria-invalidis set when the amount is out of range or abovemax, and the error message lives in arole="status"region- The copy button is
type="button"with anaria-label(which switches to "copied" after success), keyboard operable with afocus-visiblering - 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
ROC (Minguo) Date Picker (React)
A React date picker that toggles between the ROC (Minguo) calendar and the Gregorian one — a grid calendar with a "民國 115 年" (Minguo year) dropdown, typed input like "115/08/18" or "2026-08-18" parsed with an IME guard, and ISO output that also carries the ROC and Gregorian forms.
IME-Aware Search (React)
A React search box that never fires queries or lets Enter mis-select while a Zhuyin/Pinyin IME is composing — queries wait until compositionend (normal typing is debounced), a composing-state chip, highlighted matches, a / hotkey to focus, and a copy-paste IME handling pattern for your own inputs.