Price Input
A text input that live-formats digits with thousands separators as you type — e.g. typing 125000 shows 125,000 on every keystroke, not just on blur. Reports back a plain number | null, so it's trivial to send to a server. It also normalizes Persian (۰-۹) and Arabic-Indic (٠-٩) numerals to plain digits as you type or paste, since Persian keyboards commonly type those instead of 0-9 — see Normalize Persian Digits.
Last updated August 9, 2026
Overview
"use client"
import * as React from "react"
import { PriceInput } from "@/components/ui/price-input"
export function PriceInputDemoExample() {
const [value, setValue] = React.useState<number | null>(125000)
return (
<PriceInput
value={value}
onValueChange={setValue}
className="w-full max-w-48"
/>
)
}Installation
$ npx shadcn@latest add https://ui.persian-labs.ir/r/price-input.jsonUsage
import { PriceInput } from "@/components/ui/price-input"
export function Example() {
const [value, setValue] = React.useState<number | null>(125000)
return <PriceInput value={value} onValueChange={setValue} />
}Negative Amounts
Negative values are allowed by default. Pass min={0} to disallow them — the value is clamped to the given min/max range on blur.
Negative amounts are allowed by default. Pass min={0} to disallow them.
"use client"
import * as React from "react"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { PriceInput } from "@/components/ui/price-input"
export function PriceInputNegativeExample() {
const [value, setValue] = React.useState<number | null>(-15000)
return (
<Field className="w-full max-w-xs">
<FieldLabel htmlFor="price-input-negative">Balance adjustment</FieldLabel>
<PriceInput id="price-input-negative" value={value} onValueChange={setValue} />
<FieldDescription>
Negative amounts are allowed by default. Pass{" "}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
min={"{0}"}
</code>{" "}
to disallow them.
</FieldDescription>
</Field>
)
}With a Currency Suffix
Compose PriceInput with Input Group for a currency suffix like "تومان". Pass the group's control styles through className, and set data-slot="input-group-control" so it sits flush with the addon and its border/focus states bubble up to the group.
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
} from "@/components/ui/input-group"
import { PriceInput } from "@/components/ui/price-input"
export function PriceInputGroupTextExample() {
const [value, setValue] = React.useState<number | null>(450000)
return (
<InputGroup className="w-full max-w-xs">
<PriceInput
data-slot="input-group-control"
value={value}
onValueChange={setValue}
className="flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent"
/>
<InputGroupAddon align="inline-end">
<InputGroupText>تومان</InputGroupText>
</InputGroupAddon>
</InputGroup>
)
}With an Abbreviation
A short abbreviation like "T" reads well in dense UI — tables, cards, compact forms — where the full word would crowd the layout.
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
} from "@/components/ui/input-group"
import { PriceInput } from "@/components/ui/price-input"
export function PriceInputGroupAbbreviationExample() {
const [value, setValue] = React.useState<number | null>(89000)
return (
<InputGroup className="w-full max-w-xs">
<PriceInput
data-slot="input-group-control"
value={value}
onValueChange={setValue}
className="flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent"
/>
<InputGroupAddon align="inline-end">
<InputGroupText className="font-mono">T</InputGroupText>
</InputGroupAddon>
</InputGroup>
)
}With the Toman Icon
Use the Toman Icon instead of text for a more compact, symbol-first affordance.
"use client"
import * as React from "react"
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
import { PriceInput } from "@/components/ui/price-input"
import { TomanIcon } from "@/components/ui/toman-icon"
export function PriceInputGroupIconExample() {
const [value, setValue] = React.useState<number | null>(1250000)
return (
<InputGroup className="w-full max-w-xs">
<InputGroupAddon>
<TomanIcon />
</InputGroupAddon>
<PriceInput
data-slot="input-group-control"
value={value}
onValueChange={setValue}
className="flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent"
/>
</InputGroup>
)
}RTL
The formatted amount itself stays dir="ltr" — digits always read left-to-right — while the label, suffix, and description flow naturally with the RTL layout around it. Try typing Persian digits (۰-۹) below; they're normalized automatically.
اعداد فارسی (۰-۹) هم هنگام تایپ بهطور خودکار تبدیل میشوند.
"use client"
import * as React from "react"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
} from "@/components/ui/input-group"
import { PriceInput } from "@/components/ui/price-input"
export function PriceInputRtlExample() {
const [value, setValue] = React.useState<number | null>(2500000)
return (
<Field className="w-full max-w-xs">
<FieldLabel htmlFor="price-input-rtl">قیمت</FieldLabel>
<InputGroup>
<PriceInput
id="price-input-rtl"
data-slot="input-group-control"
value={value}
onValueChange={setValue}
className="flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent"
/>
<InputGroupAddon align="inline-end">
<InputGroupText>تومان</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
اعداد فارسی (۰-۹) هم هنگام تایپ بهطور خودکار تبدیل میشوند.
</FieldDescription>
</Field>
)
}