FormatInput

free

FormatInput formats numeric entry as the user types, with numeric, pattern-mask, and custom-format variants over the shared Input.

react-number-format
Preview
Dark
import FormatInput from '@/components/composites/FormatInput';

export default function UsageDemo() {
  return (
    <div className="mx-auto w-full max-w-md">
      <FormatInput.Numeric
        placeholder="0.00"
        prefix="$"
        thousandSeparator
        decimalScale={2}
        fixedDecimalScale
      />
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add FormatInput

Examples

Numeric Formats

Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
import Form from '@/components/ui/Form';

export default function NumericFormatsDemo() {
  return (
    <div className="mx-auto w-full max-w-md">
      <Form.Field label="Discount" htmlFor="discount">
        <FormatInput.Numeric
          id="discount"
          placeholder="0.0"
          suffix="%"
          decimalScale={1}
          allowNegative={false}
        />
      </Form.Field>
      <Form.Field label="Units in stock" htmlFor="units-in-stock">
        <FormatInput.Numeric
          id="units-in-stock"
          placeholder="0"
          thousandSeparator
          decimalScale={0}
          allowNegative={false}
        />
      </Form.Field>
    </div>
  );
}

Affix

Preview
Dark
Value: — the $ became part of the text
kgValue: — the icon and “kg” are decoration only
import { useState } from 'react';
import { PiTag } from 'react-icons/pi'
import FormatInput from '@/components/composites/FormatInput';

export default function AffixDemo() {
  const [amount, setAmount] = useState<number>();
  const [weight, setWeight] = useState<number>();

  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-4">
      <div className="flex flex-col gap-2">
        <FormatInput.Numeric
          placeholder="0.00"
          prefix="$"
          thousandSeparator
          decimalScale={2}
          onValueChange={(values) => setAmount(values.floatValue)}
        />
        <span className="text-xs text-muted-foreground">
          Value: {amount ?? '—'} — the $ became part of the text
        </span>
      </div>
      <div className="flex flex-col gap-2">
        <FormatInput.Numeric
          placeholder="0"
          inputPrefix={<PiTag className="text-base text-muted-foreground" />}
          inputSuffix="kg"
          onValueChange={(values) => setWeight(values.floatValue)}
        />
        <span className="text-xs text-muted-foreground">
          Value: {weight ?? '—'} — the icon and “kg” are decoration only
        </span>
      </div>
    </div>
  );
}

Stepper

Preview
Dark
import { useState } from 'react';
import FormatInput from '@/components/composites/FormatInput';

export default function StepperDemo() {
  const [seats, setSeats] = useState(1);

  return (
    <div className="mx-auto w-full max-w-md">
      <FormatInput.Numeric
        stepper
        min={1}
        max={20}
        step={1}
        value={seats}
        onValueChange={(values) => setSeats(values.floatValue ?? 1)}
      />
    </div>
  );
}

Pattern

Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
import Form from '@/components/ui/Form';

export default function PatternDemo() {
  return (
    <div className="mx-auto w-full max-w-md">
      <Form.Field label="Phone number" htmlFor="phone-number">
        <FormatInput.Pattern
          id="phone-number"
          format="(###) ###-####"
          mask="_"
          placeholder="(___) ___-____"
        />
      </Form.Field>
      <Form.Field label="ZIP + 4" htmlFor="zip-plus-4">
        <FormatInput.Pattern
          id="zip-plus-4"
          format="#####-####"
          mask="_"
          placeholder="_____-____"
        />
      </Form.Field>
    </div>
  );
}

Custom Format

Preview
Dark
import FormatInput from '@/components/composites/FormatInput';

function formatLicenseKey(value: string) {
  const clean = value
    .toUpperCase()
    .replace(/[^A-Z0-9]/g, '')
    .slice(0, 16);
  return clean.match(/.{1,4}/g)?.join('-') ?? clean;
}

function stripLicenseKey(value: string) {
  return value.replace(/[^A-Za-z0-9]/g, '');
}

export default function CustomFormatDemo() {
  return (
    <div className="mx-auto w-full max-w-md">
      <FormatInput.Custom
        placeholder="XXXX-XXXX-XXXX-XXXX"
        format={formatLicenseKey}
        removeFormatting={stripLicenseKey}
      />
    </div>
  );
}

Controlled

Preview
Dark
Raw value: 1200
import { useState } from 'react';
import Button from '@/components/ui/Button';
import FormatInput from '@/components/composites/FormatInput';

export default function ControlledDemo() {
  const [value, setValue] = useState(1200);

  return (
    <div className="mx-auto w-full max-w-md space-y-2">
      <FormatInput.Numeric
        prefix="$"
        thousandSeparator
        decimalScale={2}
        fixedDecimalScale
        value={value}
        onValueChange={(values) => setValue(values.floatValue ?? 0)}
      />
      <div className="flex items-center justify-between">
        <div className="text-muted-foreground">Raw value: {value}</div>
        <Button onClick={() => setValue(0)}>Reset</Button>
      </div>
    </div>
  );
}

API

FormatInput.Numeric, FormatInput.Pattern, and FormatInput.Custom each wrap a react-number-format primitive over the shared Input surface. All three take inputPrefix/inputSuffix instead of Input's own prefix/suffix, because Numeric and Custom already use prefix/suffix for text that's baked into the formatted value itself.

FormatInput.Numeric

PropDescriptionTypeDefault
valueControlled numeric value.number | string | null
defaultValueInitial value for uncontrolled usage.number | string | null
onValueChangeFires with the parsed float, raw digits, and formatted string whenever the value changes.(values: NumberFormatValues, sourceInfo: SourceInfo) => void
thousandSeparatorCharacter used to group digits, or true for a comma.boolean | string
decimalSeparatorCharacter shown before the decimal digits.string'.'
decimalScaleNumber of digits kept after the decimal separator.number
fixedDecimalScalePads with trailing zeros to always show decimalScale digits.booleanfalse
allowNegativeAllows a leading minus sign.booleantrue
allowLeadingZerosKeeps leading zeros instead of stripping them.booleanfalse
thousandsGroupStyleDigit grouping convention.'thousand' | 'lakh' | 'wan' | 'none''thousand'
prefixText baked into the formatted value, before the digits (for example '$').string
suffixText baked into the formatted value, after the digits (for example '%').string
stepperReplaces the suffix slot with built-in increment/decrement buttons. Requires controlled value + onValueChange; the buttons have no effect with only defaultValue.booleanfalse
minLowest value the stepper buttons will reach.number0
maxHighest value the stepper buttons will reach.numberInfinity
stepAmount each stepper click adds or subtracts.number1
inputPrefixDecorative content in Input's prefix slot; not part of the value.string | ReactNode
inputSuffixDecorative content in Input's suffix slot; not part of the value. Ignored when stepper is true.string | ReactNode
sizeControl size.ControlSizeConfigProvider controlSize
disabledPrevents editing and applies disabled control styling.boolean
...restRemaining NumericFormat props from react-number-format, plus native input attributes.NumericFormatProps

FormatInput.Pattern

PropDescriptionTypeDefault
formatPattern string where each patternChar is a fillable digit slot (for example '(###) ###-####').string
maskPlaceholder character shown in empty digit slots, or one character per slot.string | string[]
patternCharCharacter in format that marks a fillable slot.string'#'
allowEmptyFormattingShows the literal pattern characters even before any digit is entered.booleanfalse
valueControlled value.string | number | null
defaultValueInitial value for uncontrolled usage.string | number | null
onValueChangeFires with the parsed digits and formatted string whenever the value changes.(values: NumberFormatValues, sourceInfo: SourceInfo) => void
inputPrefixDecorative content in Input's prefix slot.string | ReactNode
inputSuffixDecorative content in Input's suffix slot.string | ReactNode
sizeControl size.ControlSizeConfigProvider controlSize
disabledPrevents editing and applies disabled control styling.boolean
...restRemaining PatternFormat props from react-number-format, plus native input attributes.PatternFormatProps

FormatInput.Custom

PropDescriptionTypeDefault
formatTransforms raw characters into the displayed string.(value: string) => stringidentity
removeFormattingStrips a formatted string back to raw characters, typically while editing.(value: string, changeMeta?: ChangeMeta) => stringdigits-only
getCaretBoundaryMarks which caret positions are safe to land on inside the formatted string.(formattedValue: string) => boolean[]numeric-boundary heuristic
valueControlled value.string | number | null
defaultValueInitial value for uncontrolled usage.string | number | null
onValueChangeFires with the parsed value and formatted string whenever the value changes.(values: NumberFormatValues, sourceInfo: SourceInfo) => void
inputPrefixDecorative content in Input's prefix slot.string | ReactNode
inputSuffixDecorative content in Input's suffix slot.string | ReactNode
sizeControl size.ControlSizeConfigProvider controlSize
disabledPrevents editing and applies disabled control styling.boolean
...restRemaining NumberFormatBase props from react-number-format, plus native input attributes.NumberFormatBaseProps

NumericFormatProps, PatternFormatProps, and NumberFormatBaseProps are react-number-format's own types; see the library's docs for its full option list.

Types

type NumberFormatValues = {
  floatValue: number | undefined;
  formattedValue: string;
  value: string;
};
 
type SourceInfo = {
  event?: SyntheticEvent<HTMLInputElement>;
  source: 'event' | 'prop';
};
 
type ChangeMeta = {
  from: { start: number; end: number };
  to: { start: number; end: number };
  lastValue: string;
};
 
type ControlSize = 'sm' | 'md' | 'lg';