- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"use client";
import { InputAmount } from "@/components/ui/input-amount";
import { useState } from "react";
export function InputAmountDemo() {
const [amount, setAmount] = useState("1250000.50");
return (
<>
<InputAmount
aria-label={"Amount"}
value={amount}
onValueChange={setAmount}
prefix={"Rp "}
suffix={""}
thousandSeparator={" "}
decimalSeparator={","}
decimalScale={2}
/>
</>
);
}Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/input-amount.json
The default installation target is components/ui/input-amount.tsx. Review the CLI's overwrite prompt before replacing local customizations.
Dependencies
Installed automatically by the registry CLI. For manual setup:
- cn
- react-number-format
$ pnpm add cn react-number-formatUsage
import { InputAmount } from "@/components/ui/input-amount";
<InputAmount
aria-label="Amount in Rupiah"
prefix="Rp "
thousandSeparator="."
decimalSeparator=","
value={amount}
onValueChange={setAmount}
/>;Keep the value as decimal text, for example "1234567.89". Configure separators explicitly for the display you want. Use decimalScale to constrain fractional digits; fixedDecimalScale affects display padding only.
Examples
The value readout is the exact raw decimal string, not the localized display text.
"use client";
import { useState } from "react";
import { InputAmount } from "@/components/ui/input-amount";
export const InputAmountDemo = () => {
const [amount, setAmount] = useState("1250000.50");
return (
<div className="grid max-w-sm gap-2">
<InputAmount
aria-label="Amount in Rupiah"
value={amount}
onValueChange={setAmount}
prefix="Rp "
thousandSeparator="."
decimalSeparator=","
decimalScale={2}
/>
<output className="text-sm text-muted-foreground">
Raw decimal value: {amount}
</output>
</div>
);
};Optional Storybook Stories
Install Input Amount first, then run npx shadcn@latest add https://vandor-ui.vercel.app/r/input-amount-stories.json. Stories cover decimal-comma formatting, integer and negative amounts, controlled decimal-string output, and disabled/read-only/invalid states. Controls offer a space or no thousands separator, avoiding a collision when you change the decimal separator.
Stories install beside Input Amount under your configured UI alias and use relative imports. Use your existing matching @storybook/react packages (verified with 10.6.1), discover **/*.stories.tsx, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Default-value Controls remount uncontrolled examples; Controlled owns local state. Storybook UI is not verified because no server exists here.
Behavior & Accessibility
Formatting preserves normal caret and paste editing. Values are never converted to floating-point numbers. onValueChange receives raw decimal text with . as the decimal separator, or "" when cleared. The formatted display is not a substitute for caller-owned form serialization.
Props
| Prop | Type / Description | Default |
|---|---|---|
| allowNegative | booleanInputAmount: allows a leading minus sign. | false |
| aria-label | stringInputAmount: accessible name for the native input. | required without label |
| decimalScale | numberInputAmount: maximum fraction digits. | undefined |
| decimalSeparator | stringInputAmount: displayed decimal separator. | "." |
| defaultValue | stringInputAmount: initial raw decimal text. | undefined |
| fixedDecimalScale | booleanInputAmount: pads display to decimalScale. | false |
| name | stringInputAmount: native form field name. | undefined |
| onValueChange | (value: string) => voidInputAmount: receives raw decimal text on user edits. | undefined |
| prefix | stringInputAmount: text displayed before the formatted number. | "" |
| suffix | stringInputAmount: text displayed after the formatted number. | "" |
| thousandSeparator | string | booleanInputAmount: optional display grouping separator. | false |
| value | stringInputAmount: controlled raw decimal text; decimal mark is always a period. | uncontrolled |
The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.
Source
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import { NumericFormat } from "react-number-format";
import type { NumericFormatProps } from "react-number-format";
import { fieldSurfaceClassName } from "./input";
export type InputAmountProps = Omit<
NumericFormatProps<ComponentProps<"input">>,
| "value"
| "defaultValue"
| "onValueChange"
| "thousandSeparator"
| "decimalSeparator"
| "prefix"
| "suffix"
| "decimalScale"
| "fixedDecimalScale"
| "allowNegative"
> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
thousandSeparator?: string | boolean;
decimalSeparator?: string;
prefix?: string;
suffix?: string;
decimalScale?: number;
fixedDecimalScale?: boolean;
allowNegative?: boolean;
};
const fieldClassName =
"h-9 w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive dark:bg-input/30 motion-reduce:transition-none";
export const InputAmount = ({
className,
thousandSeparator = false,
decimalSeparator = ".",
prefix = "",
suffix = "",
decimalScale,
fixedDecimalScale = false,
allowNegative = false,
onValueChange,
...props
}: InputAmountProps) => {
if (
typeof thousandSeparator === "string" &&
thousandSeparator === decimalSeparator
) {
throw new Error(
"thousandSeparator and decimalSeparator must be different."
);
}
return (
<NumericFormat
{...props}
data-slot="amount-input"
thousandSeparator={thousandSeparator}
decimalSeparator={decimalSeparator}
prefix={prefix}
suffix={suffix}
decimalScale={decimalScale}
fixedDecimalScale={fixedDecimalScale}
allowNegative={allowNegative}
onValueChange={(values, sourceInfo) => {
if (sourceInfo.source === "event") {
onValueChange?.(values.value);
}
}}
className={cn(fieldClassName, fieldSurfaceClassName, className)}
/>
);
};