Input Formatters: Restricting & Shaping Text
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.
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
replacementStringto substitute for removed characters (defaults to the empty string). - Flutter ships two named constructors:
FilteringTextInputFormatter.digitsOnlyandFilteringTextInputFormatter.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)'),
),
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',
),
)
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.allowto whitelist acceptable characters viaRegExp. - Use
FilteringTextInputFormatter.denyto blacklist unwanted characters. - Use
LengthLimitingTextInputFormatterto cap field length without displaying a counter. - Extend
TextInputFormatterand overrideformatEditUpdatefor advanced masking (phones, dates, credit cards). - Always return a complete
TextEditingValue— including a validselection— from custom formatters.
TextInputFormatter subclass covers virtually every text-shaping requirement you will encounter in production Flutter apps.