Numeric Input
Number input with formatting, thousands separators, and currency support
Overview
The Numeric Input component provides a formatted number input field with support for thousands separators, decimal marks, currency symbols, and configurable precision. It handles both focused (raw) and blurred (formatted) states.
Installation
npx shadcn@latest add https://foodease-dev-registry.cap.reachcinema.io/r/v1/numeric-input.jsonThis will install:
- Numeric Input component
- Input component (dependency)
Usage
Basic Usage
import { NumericInput } from "@/components/ui/numeric-input"
export default function Example() {
const [value, setValue] = useState<number | undefined>()
return (
<NumericInput
value={value}
onChange={setValue}
placeholder="Enter amount"
/>
)
}With Currency Prefix
import { NumericInput } from "@/components/ui/numeric-input"
export default function PriceInput() {
const [price, setPrice] = useState<number>()
return (
<NumericInput
value={price}
onChange={setPrice}
config={{
prefix: "$",
precision: 2,
}}
placeholder="0.00"
/>
)
}With Custom Separators
import { NumericInput } from "@/components/ui/numeric-input"
export default function EuropeanFormat() {
const [amount, setAmount] = useState<number>()
return (
<NumericInput
value={amount}
onChange={setAmount}
config={{
separator: ".",
decimalMark: ",",
prefix: "€",
}}
placeholder="0,00"
/>
)
}Percentage Input
import { NumericInput } from "@/components/ui/numeric-input"
export default function PercentageInput() {
const [percent, setPercent] = useState<number>()
return (
<NumericInput
value={percent}
onChange={setPercent}
config={{
suffix: "%",
precision: 1,
}}
placeholder="0.0"
/>
)
}Integer Only
import { NumericInput } from "@/components/ui/numeric-input"
export default function QuantityInput() {
const [quantity, setQuantity] = useState<number>()
return (
<NumericInput
value={quantity}
onChange={setQuantity}
config={{
precision: 0,
allowNegative: false,
}}
placeholder="0"
/>
)
}API Reference
NumericInput
| Prop | Type | Description |
|---|---|---|
value | number | undefined | Controlled value (numeric) |
defaultValue | number | Default value for uncontrolled mode |
onChange | (value: number | undefined) => void | Called when value changes |
config | NumericConfig | Formatting configuration |
className | string | Additional CSS classes |
| ...props | InputHTMLAttributes | All standard input props |
NumericConfig
| Property | Type | Default | Description |
|---|---|---|---|
separator | string | "," | Thousands separator |
decimalMark | string | "." | Decimal point character |
prefix | string | "" | Text before number (e.g., "$") |
suffix | string | "" | Text after number (e.g., "%") |
precision | number | 2 | Number of decimal places |
allowNegative | boolean | true | Allow negative values |
Behavior
Focus States
Focused (Editing):
- Raw numeric value without formatting
- No thousands separators
- No prefix/suffix
Blurred (Display):
- Fully formatted with separators
- Includes prefix/suffix
- Fixed decimal precision
Example
// While focused: "12345.67"
// While blurred: "$12,345.67"Examples
Currency Input with Validation
import { NumericInput } from "@/components/ui/numeric-input"
import { useState } from "react"
export default function PriceField() {
const [price, setPrice] = useState<number>()
const isValid = price !== undefined && price > 0
return (
<div className="space-y-2">
<label>Price</label>
<NumericInput
value={price}
onChange={setPrice}
config={{
prefix: "$",
precision: 2,
allowNegative: false,
}}
className={!isValid && price !== undefined ? "border-red-500" : ""}
placeholder="0.00"
/>
{!isValid && price !== undefined && (
<p className="text-sm text-red-500">Price must be greater than 0</p>
)}
</div>
)
}Multi-Currency Form
import { NumericInput } from "@/components/ui/numeric-input"
import { useState } from "react"
export default function PricingForm() {
const [usd, setUsd] = useState<number>()
const [eur, setEur] = useState<number>()
const [gbp, setGbp] = useState<number>()
return (
<div className="space-y-4">
<div>
<label>Price (USD)</label>
<NumericInput
value={usd}
onChange={setUsd}
config={{ prefix: "$", precision: 2 }}
/>
</div>
<div>
<label>Price (EUR)</label>
<NumericInput
value={eur}
onChange={setEur}
config={{ prefix: "€", separator: ".", decimalMark: "," }}
/>
</div>
<div>
<label>Price (GBP)</label>
<NumericInput
value={gbp}
onChange={setGbp}
config={{ prefix: "£", precision: 2 }}
/>
</div>
</div>
)
}Calculation Form
import { NumericInput } from "@/components/ui/numeric-input"
import { useState } from "react"
export default function Calculator() {
const [quantity, setQuantity] = useState<number>(1)
const [price, setPrice] = useState<number>(0)
const [taxRate, setTaxRate] = useState<number>(10)
const subtotal = (quantity || 0) * (price || 0)
const tax = subtotal * ((taxRate || 0) / 100)
const total = subtotal + tax
return (
<div className="space-y-4">
<div>
<label>Quantity</label>
<NumericInput
value={quantity}
onChange={setQuantity}
config={{ precision: 0, allowNegative: false }}
/>
</div>
<div>
<label>Unit Price</label>
<NumericInput
value={price}
onChange={setPrice}
config={{ prefix: "$", precision: 2 }}
/>
</div>
<div>
<label>Tax Rate</label>
<NumericInput
value={taxRate}
onChange={setTaxRate}
config={{ suffix: "%", precision: 1 }}
/>
</div>
<div className="border-t pt-4 space-y-2">
<div className="flex justify-between">
<span>Subtotal:</span>
<span className="font-semibold">${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>Tax:</span>
<span className="font-semibold">${tax.toFixed(2)}</span>
</div>
<div className="flex justify-between text-lg">
<span>Total:</span>
<span className="font-bold">${total.toFixed(2)}</span>
</div>
</div>
</div>
)
}Measurement Input
import { NumericInput } from "@/components/ui/numeric-input"
import { useState } from "react"
export default function MeasurementForm() {
const [weight, setWeight] = useState<number>()
const [height, setHeight] = useState<number>()
const [distance, setDistance] = useState<number>()
return (
<div className="space-y-4">
<div>
<label>Weight</label>
<NumericInput
value={weight}
onChange={setWeight}
config={{ suffix: " kg", precision: 1 }}
placeholder="0.0"
/>
</div>
<div>
<label>Height</label>
<NumericInput
value={height}
onChange={setHeight}
config={{ suffix: " cm", precision: 0 }}
placeholder="0"
/>
</div>
<div>
<label>Distance</label>
<NumericInput
value={distance}
onChange={setDistance}
config={{ suffix: " km", precision: 2 }}
placeholder="0.00"
/>
</div>
</div>
)
}Uncontrolled with Default Value
import { NumericInput } from "@/components/ui/numeric-input"
export default function DefaultValueExample() {
return (
<NumericInput
defaultValue={100}
config={{ prefix: "$", precision: 2 }}
onChange={(value) => console.log("New value:", value)}
/>
)
}Features
- Smart Formatting: Automatically formats on blur, raw input on focus
- Thousands Separators: Configurable separators (comma, period, space)
- Decimal Support: Configurable decimal mark and precision
- Prefix/Suffix: Add currency symbols, units, or percentages
- Negative Values: Optional negative number support
- Type Safety: Returns
number | undefined(not strings) - Controlled & Uncontrolled: Supports both modes
- Accessible: Built on standard input element
Notes
- Value is always stored as a number, not a string
- Formatting only applies when input is not focused
- Invalid input returns
undefinedto onChange - Component automatically strips non-numeric characters
- Respects precision setting when formatting
Dependencies
@/components/ui/input- Base input component@/lib/utils- cn utility function