3

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.json

Usage

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.

T
"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>
  )
}

API Reference

PriceInput