ThousandsSeparatorTextInputFormatter class

The ThousandsSeparatorTextInputFormatter is a TextInputFormatter that formats numeric input with thousands separators while the user types, and preserves the caret or selection as naturally as possible.

This formatter is intended for text fields that accept:

  • integer values like 123456 -> 123,456
  • decimal values like 123456.32 -> 123,456.32

Features

  • Adds thousands separators as you type.
  • When digits are inserted or deleted, including in the middle, separators are recalculated (re-positioned).
  • You can add a fractional part by typing a dot . and then more digits.
  • Only digits and one decimal dot are kept. Commas, minus signs, and other characters are ignored.
  • If a dot already exists and you type another one to the right, the new one is ignored.
  • If a dot already exists and you type another one to the left, the left one is kept and the old right one is dropped.
  • It tries to keep the caret or selection in the natural place after reformatting.
  • It has special handling for Backspace just to the right of a thousands separator and Delete just to the left of one, so those keys delete the neighboring digit instead of appearing to do nothing.
  • If the input starts with ., it normalizes it to 0.. For example, .5 becomes 0.5.
  • Limit the fractional part to allowedDecimals digits.
  • Prevent leading zeroes on the left.

Details

The integer part is grouped using groupSeparator, and the fractional part, if present, is left exactly as typed after the first decimalSeparator.

Example behavior:

  • 1 -> 1
  • 1234 -> 1,234
  • 12345.6 -> 12,345.6
  • 12345.60 -> 12,345.60
  • .5 -> 0.5

This formatter also handles editing in the middle of the text, not only at the end. For example, if the field contains 123,456.32, the user places the caret between 1 and 2, and types 5, the result becomes 1,523,456.32, and the caret stays immediately after the inserted 5.

How it works

The formatter treats the visible text and the numeric value as two different representations:

  • the formatted text shown in the field, such as 1,234,567.89
  • a raw numeric text without grouping separators, such as 1234567.89

On each edit, it performs these steps:

  1. Sanitize the user's edited text into raw numeric text. Only digits and a single decimal separator are kept.
  2. Convert the current selection from formatted text offsets into raw text offsets.
  3. Rebuild the formatted text from the raw text by inserting grouping separators into the integer part.
  4. Convert the selection back from raw offsets into formatted offsets.
  5. Return a new TextEditingValue with the formatted text and corrected selection.

The important idea is that the selection is mapped through the raw text, instead of trying to guess its final visible position directly. This makes caret placement much more reliable when separators are inserted or removed during formatting.

Caret and selection mapping

Caret placement is based on text boundaries, not characters. A string of length N has N + 1 valid caret positions. The formatter builds mapping tables between:

  • formatted text boundaries -> raw text boundaries
  • raw text boundaries -> formatted text boundaries

Because of that, both collapsed selections, meaning a caret, and non collapsed selections, meaning highlighted ranges, can be handled using the same logic.

This is especially useful when:

  • typing in the middle of the number
  • replacing a selected range
  • pasting text
  • editing values that already contain grouping separators

Special handling for Backspace and Delete near separators

Grouping separators are formatting characters only. They do not exist in the raw numeric value. Because of that, deleting a comma naively would appear to do nothing, since the formatter would simply insert it again when rebuilding the formatted text.

To avoid that, the formatter compares oldValue and newValue and tries to recover the user's intent when the only removed characters are grouping separators and the raw numeric value did not actually change.

Two important cases are handled:

  • Backspace immediately to the right of a grouping separator
  • Delete immediately to the left of a grouping separator

In those cases, the formatter interprets the edit as a request to delete the adjacent digit in raw space, then reformats the result.

Example:

  • 123,456, caret after ,, Backspace -> 12,456
  • 123,456, caret before ,, Delete -> 12,356

This makes deletion feel much closer to what users expect in a formatted numeric field.

Composition and IME behavior

If the input method is currently composing text, the formatter returns newValue unchanged. This avoids interfering with active IME composition, which is important for correct text input behavior on mobile keyboards and some international input methods.

Normalization rules

The formatter applies a few normalization rules:

  • Only digits are kept, plus the first decimal separator.
  • Additional decimal separators are ignored.
  • If the input starts with the decimal separator, it is normalized to a leading zero. For example, .5 becomes 0.5.
  • Grouping separators in the input are ignored during parsing and are always rebuilt from the raw value.

Scope and assumptions

This formatter is designed for numeric text entry with:

  • ASCII digits 0 through 9
  • a single character grouping separator
  • a single character decimal separator

It does not attempt to support:

  • negative numbers
  • scientific notation
  • locale specific digits
  • currency symbols
  • arbitrary user text

Direct string indexing is used internally, which is safe for this formatter because it intentionally works only with simple ASCII numeric input and single character separators.

Typical usage

TextField(
  keyboardType: const TextInputType.numberWithOptions(decimal: true),
  inputFormatters: [
    GroupedDecimalInputFormatter(),
  ],
)

By default, this formatter uses , as the grouping separator and . as the decimal separator.

Inheritance

Constructors

ThousandsSeparatorTextInputFormatter({String groupSeparator = ',', String decimalSeparator = '.', int allowedDecimals = 1000})

Properties

allowedDecimals int
final
decimalSeparator String
final
groupSeparator String
final
hashCode int
The hash code for this object.
no setterinherited
runtimeType Type
A representation of the runtime type of the object.
no setterinherited

Methods

formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) TextEditingValue
Called when text is being typed or cut/copy/pasted in the EditableText.
override
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toString() String
A string representation of this object.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited