For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Input Amount

A formatted decimal input that emits exact raw decimal strings.

Playground

Live preview
Raw decimal value: 1250000.50
Edit the field to compare localized formatting with the raw decimal string emitted to your application.
Customize input-amount
Customize
input-amount-demo.tsx
"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:

Dependencies2 required packages
  • cn
  • react-number-format
$ pnpm add cn react-number-format

Usage

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.

Raw decimal value: 1250000.50
input-amount-demo.tsx
"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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
allowNegativeboolean

InputAmount: allows a leading minus sign.

false
aria-labelstring

InputAmount: accessible name for the native input.

required without label
decimalScalenumber

InputAmount: maximum fraction digits.

undefined
decimalSeparatorstring

InputAmount: displayed decimal separator.

"."
defaultValuestring

InputAmount: initial raw decimal text.

undefined
fixedDecimalScaleboolean

InputAmount: pads display to decimalScale.

false
namestring

InputAmount: native form field name.

undefined
onValueChange(value: string) => void

InputAmount: receives raw decimal text on user edits.

undefined
prefixstring

InputAmount: text displayed before the formatted number.

""
suffixstring

InputAmount: text displayed after the formatted number.

""
thousandSeparatorstring | boolean

InputAmount: optional display grouping separator.

false
valuestring

InputAmount: 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)}
    />
  );
};