Forms, Validation & User Input

Input Formatters: Restricting & Shaping Text

15 min Lesson 4 of 12

Input Formatters: Restricting & Shaping Text

Flutter's TextField and TextFormField accept an inputFormatters parameter — a list of TextInputFormatter objects that intercept every keystroke and can filter, replace, or reshape the value before it ever reaches the field's display buffer. This gives you precise, real-time control over what the user can type, far beyond what keyboardType alone provides.

Note: Input formatters run synchronously on every character change. They receive the old TextEditingValue and the new proposed value, and they return the value that should actually be used. Returning the old value effectively rejects the change.

FilteringTextInputFormatter

FilteringTextInputFormatter is the most commonly used built-in formatter. It uses a RegExp to either allow (whitelist) or deny (blacklist) specific characters.

  • FilteringTextInputFormatter.allow(pattern) — keeps only characters matching the pattern.
  • FilteringTextInputFormatter.deny(pattern) — removes any characters matching the pattern.
  • Both constructors accept an optional replacementString to substitute for removed characters (defaults to the empty string).
  • Flutter ships two named constructors: FilteringTextInputFormatter.digitsOnly and FilteringTextInputFormatter.singleLineFormatter.

Common FilteringTextInputFormatter Examples

import 'package:flutter/services.dart';

// Allow digits only (same as FilteringTextInputFormatter.digitsOnly)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
  ],
  keyboardType: TextInputType.number,
  decoration: const InputDecoration(labelText: 'Age'),
),

// Allow letters and spaces only (no numbers or symbols)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z؀-ۿ ]')),
  ],
  decoration: const InputDecoration(labelText: 'Full Name'),
),

// Deny special characters (blacklist approach)
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.deny(RegExp(r'[!@#\$%^&*()]')),
  ],
  decoration: const InputDecoration(labelText: 'Username'),
),

LengthLimitingTextInputFormatter

LengthLimitingTextInputFormatter caps the number of characters the field will accept. While you can also set maxLength on the TextField itself, using the formatter gives you the limit without rendering the character counter below the field — a clean choice for UI-sensitive designs.

Combining Multiple Formatters

import 'package:flutter/services.dart';

// OTP / PIN field: digits only, max 6 characters
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(6),
  ],
  keyboardType: TextInputType.number,
  obscureText: true,
  decoration: const InputDecoration(labelText: 'Enter OTP'),
),

// Alphanumeric username: letters + digits, max 20 chars
TextField(
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]')),
    LengthLimitingTextInputFormatter(20),
  ],
  decoration: const InputDecoration(labelText: 'Username (max 20)'),
),
Tip: Always add LengthLimitingTextInputFormatter after the filtering formatter in the list. Formatters execute in order; if the length formatter runs first it might block valid characters before the filter can evaluate them — though in practice either order works for most cases. Making the order intentional keeps the code readable.

Writing a Custom TextInputFormatter

For more sophisticated reshaping — such as inserting dashes in a phone number, adding slashes to a date, or uppercasing the first letter — you extend TextInputFormatter and override the single required method formatEditUpdate.

The method signature is:

TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue)

You receive both the previous and proposed values (including cursor position) and must return the final TextEditingValue — including an updated cursor position to keep it from jumping unexpectedly.

Custom Phone Number Formatter (###-###-####)

import 'package:flutter/services.dart';

/// Formats a 10-digit US phone number as ###-###-####
/// while the user types.
class PhoneNumberFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // Strip every non-digit character first
    final digitsOnly = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');

    // Cap at 10 digits
    final capped = digitsOnly.length > 10
        ? digitsOnly.substring(0, 10)
        : digitsOnly;

    // Build the masked string
    final buffer = StringBuffer();
    for (int i = 0; i < capped.length; i++) {
      if (i == 3 || i == 6) buffer.write('-');
      buffer.write(capped[i]);
    }

    final formatted = buffer.toString();

    return TextEditingValue(
      text: formatted,
      // Place cursor at the end of the formatted string
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

// Usage in a widget:
TextField(
  inputFormatters: [PhoneNumberFormatter()],
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'Phone Number',
    hintText: '555-867-5309',
  ),
)
Warning: Always update the selection (cursor position) in your custom formatter's return value. If you return only TextEditingValue(text: formatted) without a selection, the cursor will jump to offset 0 on every keystroke, making the field nearly unusable.

Credit Card Number Formatter Example

A second common pattern is grouping digits with spaces — used for credit card numbers (#### #### #### ####). The logic is identical to the phone formatter but with different grouping rules.

Credit Card Formatter (#### #### #### ####)

class CreditCardFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    final digits = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
    final capped = digits.length > 16 ? digits.substring(0, 16) : digits;

    final buffer = StringBuffer();
    for (int i = 0; i < capped.length; i++) {
      if (i != 0 && i % 4 == 0) buffer.write(' ');
      buffer.write(capped[i]);
    }

    final formatted = buffer.toString();
    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

Summary

Input formatters are a powerful, low-ceremony way to enforce data integrity at the UI layer:

  • Use FilteringTextInputFormatter.allow to whitelist acceptable characters via RegExp.
  • Use FilteringTextInputFormatter.deny to blacklist unwanted characters.
  • Use LengthLimitingTextInputFormatter to cap field length without displaying a counter.
  • Extend TextInputFormatter and override formatEditUpdate for advanced masking (phones, dates, credit cards).
  • Always return a complete TextEditingValue — including a valid selection — from custom formatters.
Key Takeaway: Input formatters intercept text before it is committed to the field, giving you real-time, character-by-character control. Combining built-in formatters with a custom TextInputFormatter subclass covers virtually every text-shaping requirement you will encounter in production Flutter apps.