FoodEase

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

This 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

PropTypeDescription
valuenumber | undefinedControlled value (numeric)
defaultValuenumberDefault value for uncontrolled mode
onChange(value: number | undefined) => voidCalled when value changes
configNumericConfigFormatting configuration
classNamestringAdditional CSS classes
...propsInputHTMLAttributesAll standard input props

NumericConfig

PropertyTypeDefaultDescription
separatorstring","Thousands separator
decimalMarkstring"."Decimal point character
prefixstring""Text before number (e.g., "$")
suffixstring""Text after number (e.g., "%")
precisionnumber2Number of decimal places
allowNegativebooleantrueAllow 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 undefined to onChange
  • Component automatically strips non-numeric characters
  • Respects precision setting when formatting

Dependencies

  • @/components/ui/input - Base input component
  • @/lib/utils - cn utility function

On this page