Input Formatters
Understand how to format and restrict user input in Flutter.
What is it?
Input Formatters are a powerful feature in Flutter that allow you to control, restrict, and format user input as they type. They act as filters that process each keystroke, allowing you to enforce specific formats, limit character length, allow only certain characters, and automatically format the input. This is essential for creating user-friendly input fields.
Why does it exist?
Input Formatters exist to:
- Restrict input to specific characters (e.g., only digits)
- Enforce maximum character limits
- Auto-format input (e.g., phone numbers, dates)
- Prevent invalid input before submission
- Improve user experience with real-time formatting
- Reduce validation errors
- Handle specialized input formats
Basic Input Formatters
Using input formatters to restrict input.
// Import required packages
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Basic input formatter example
class BasicInputFormatterExample extends StatefulWidget {
const BasicInputFormatterExample({super.key});
@override
State<BasicInputFormatterExample> createState() => _BasicInputFormatterExampleState();
}
class _BasicInputFormatterExampleState extends State<BasicInputFormatterExample> {
// Controllers for each field
final TextEditingController _digitsController = TextEditingController();
final TextEditingController _lettersController = TextEditingController();
final TextEditingController _alphanumericController = TextEditingController();
final TextEditingController _lengthController = TextEditingController();
@override
void dispose() {
_digitsController.dispose();
_lettersController.dispose();
_alphanumericController.dispose();
_lengthController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Input Formatters'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Only digits (0-9)
TextField(
controller: _digitsController,
decoration: const InputDecoration(
labelText: 'Only Digits',
hintText: 'Enter numbers only',
border: OutlineInputBorder(),
helperText: 'Only 0-9 allowed',
),
keyboardType: TextInputType.number,
// 1. FilteringTextInputFormatter restricts characters
// This allows only digits
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
const SizedBox(height: 16),
// 2. Only letters (A-Z, a-z)
TextField(
controller: _lettersController,
decoration: const InputDecoration(
labelText: 'Only Letters',
hintText: 'Enter letters only',
border: OutlineInputBorder(),
helperText: 'Only A-Z and a-z allowed',
),
// 2. Custom pattern for letters only
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z]'),
),
],
),
const SizedBox(height: 16),
// 3. Alphanumeric (letters and numbers)
TextField(
controller: _alphanumericController,
decoration: const InputDecoration(
labelText: 'Alphanumeric',
hintText: 'Enter letters and numbers',
border: OutlineInputBorder(),
helperText: 'A-Z, a-z, 0-9 allowed',
),
// 3. Custom pattern for alphanumeric
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9]'),
),
],
),
const SizedBox(height: 16),
// 4. Length limiting
TextField(
controller: _lengthController,
decoration: const InputDecoration(
labelText: 'Max 10 Characters',
hintText: 'Type something...',
border: OutlineInputBorder(),
helperText: 'Maximum 10 characters',
),
// 4. LengthLimitingTextInputFormatter limits characters
inputFormatters: [
LengthLimitingTextInputFormatter(10),
],
),
],
),
),
);
}
}
What's happening here? - FilteringTextInputFormatter.digitsOnly: Only digits - FilteringTextInputFormatter.allow: Custom allowed pattern - FilteringTextInputFormatter.deny: Block specific pattern - LengthLimitingTextInputFormatter: Max length
Formatters for Specific Formats
Creating formatted input fields.
/// 1. Phone number formatter
class PhoneInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Get the new text without formatting
final text = newValue.text.replaceAll(RegExp(r'[\s\-()]'), '');
// Only allow digits
if (text.isEmpty) {
return newValue.copyWith(text: '');
}
// Format as (XXX) XXX-XXXX
final buffer = StringBuffer();
// Add opening parenthesis
buffer.write('(');
// Add first 3 digits
final firstThree = text.substring(0, text.length > 3 ? 3 : text.length);
buffer.write(firstThree);
// Close parenthesis and add space after 3 digits
if (text.length > 3) {
buffer.write(') ');
final remaining = text.substring(3);
final nextThree = remaining.length > 3 ? 3 : remaining.length;
buffer.write(remaining.substring(0, nextThree));
}
// Add dash and last 4 digits
if (text.length > 6) {
buffer.write('-');
final remaining = text.substring(6);
buffer.write(remaining);
}
return newValue.copyWith(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
/// 2. Credit card number formatter
class CreditCardInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Remove all non-digits
final text = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
if (text.isEmpty) {
return newValue.copyWith(text: '');
}
// Format as XXXX XXXX XXXX XXXX
final buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
if (i > 0 && i % 4 == 0) {
buffer.write(' ');
}
buffer.write(text[i]);
}
return newValue.copyWith(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
/// 3. Date formatter (MM/DD/YYYY)
class DateInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Remove all non-digits
final text = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
if (text.isEmpty) {
return newValue.copyWith(text: '');
}
// Format as MM/DD/YYYY
final buffer = StringBuffer();
for (int i = 0; i < text.length && i < 8; i++) {
if (i == 2 || i == 4) {
buffer.write('/');
}
buffer.write(text[i]);
}
return newValue.copyWith(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
/// 4. Currency formatter
class CurrencyInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Remove all non-digits
final text = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
if (text.isEmpty) {
return newValue.copyWith(text: '');
}
// Format as $X,XXX.XX
final number = int.parse(text);
final formatted = '${(number / 100).toStringAsFixed(2)}';
// Add commas and dollar sign
final parts = formatted.split('.');
final dollars = parts[0];
final cents = parts[1];
// Add commas to dollars
final buffer = StringBuffer('\$');
for (int i = 0; i < dollars.length; i++) {
if (i > 0 && (dollars.length - i) % 3 == 0) {
buffer.write(',');
}
buffer.write(dollars[i]);
}
buffer.write('.$cents');
return newValue.copyWith(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
/// Using custom formatters
class CustomFormatterExample extends StatefulWidget {
const CustomFormatterExample({super.key});
@override
State<CustomFormatterExample> createState() => _CustomFormatterExampleState();
}
class _CustomFormatterExampleState extends State<CustomFormatterExample> {
// Controllers for formatted fields
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _cardController = TextEditingController();
final TextEditingController _dateController = TextEditingController();
final TextEditingController _currencyController = TextEditingController();
@override
void dispose() {
_phoneController.dispose();
_cardController.dispose();
_dateController.dispose();
_currencyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Formatters'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Phone number with custom formatter
TextField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Phone Number',
hintText: '(XXX) XXX-XXXX',
border: OutlineInputBorder(),
helperText: 'Format: (XXX) XXX-XXXX',
),
keyboardType: TextInputType.phone,
inputFormatters: [
PhoneInputFormatter(),
// Also limit to 14 characters (with formatting)
LengthLimitingTextInputFormatter(14),
],
),
const SizedBox(height: 16),
// 2. Credit card with custom formatter
TextField(
controller: _cardController,
decoration: const InputDecoration(
labelText: 'Credit Card',
hintText: 'XXXX XXXX XXXX XXXX',
border: OutlineInputBorder(),
helperText: 'Format: XXXX XXXX XXXX XXXX',
),
keyboardType: TextInputType.number,
inputFormatters: [
CreditCardInputFormatter(),
LengthLimitingTextInputFormatter(19),
],
),
const SizedBox(height: 16),
// 3. Date with custom formatter
TextField(
controller: _dateController,
decoration: const InputDecoration(
labelText: 'Date',
hintText: 'MM/DD/YYYY',
border: OutlineInputBorder(),
helperText: 'Format: MM/DD/YYYY',
),
keyboardType: TextInputType.number,
inputFormatters: [
DateInputFormatter(),
LengthLimitingTextInputFormatter(10),
],
),
const SizedBox(height: 16),
// 4. Currency with custom formatter
TextField(
controller: _currencyController,
decoration: const InputDecoration(
labelText: 'Amount',
hintText: '$0.00',
border: OutlineInputBorder(),
helperText: 'Format: $X,XXX.XX',
),
keyboardType: TextInputType.number,
inputFormatters: [
CurrencyInputFormatter(),
],
),
],
),
),
);
}
}
What's happening here? - Phone number formatting (XXX) XXX-XXXX - Credit card formatting XXXX XXXX XXXX XXXX - Date formatting MM/DD/YYYY - Currency formatting $X,XXX.XX
Combining Formatters
Using multiple formatters together.
/// Combining multiple formatters
class CombinedFormatterExample extends StatefulWidget {
const CombinedFormatterExample({super.key});
@override
State<CombinedFormatterExample> createState() => _CombinedFormatterExampleState();
}
class _CombinedFormatterExampleState extends State<CombinedFormatterExample> {
// Controllers for combined formatters
final TextEditingController _controller1 = TextEditingController();
final TextEditingController _controller2 = TextEditingController();
final TextEditingController _controller3 = TextEditingController();
@override
void dispose() {
_controller1.dispose();
_controller2.dispose();
_controller3.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Combined Formatters'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. Uppercase only with length limit
TextField(
controller: _controller1,
decoration: const InputDecoration(
labelText: 'Uppercase + Limit',
hintText: 'Uppercase only, max 10 chars',
border: OutlineInputBorder(),
helperText: 'A-Z only, uppercase, max 10',
),
inputFormatters: [
// 1. Allow only uppercase letters
FilteringTextInputFormatter.allow(
RegExp(r'[A-Z]'),
),
// 2. Limit length
LengthLimitingTextInputFormatter(10),
],
),
const SizedBox(height: 16),
// 2. Alphanumeric with formatting
TextField(
controller: _controller2,
decoration: const InputDecoration(
labelText: 'Alphanumeric + Format',
hintText: 'ABC-123-XYZ',
border: OutlineInputBorder(),
helperText: 'Format: ABC-123-XYZ',
),
inputFormatters: [
// 1. Allow alphanumeric
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9]'),
),
// 2. Auto-capitalize
TextInputFormatter.withFunction(
(oldValue, newValue) {
return newValue.copyWith(
text: newValue.text.toUpperCase(),
);
},
),
// 3. Length limit
LengthLimitingTextInputFormatter(11),
],
),
const SizedBox(height: 16),
// 3. Price with formatters
TextField(
controller: _controller3,
decoration: const InputDecoration(
labelText: 'Price',
hintText: '$0.00',
border: OutlineInputBorder(),
helperText: 'Format: $X,XXX.XX',
),
keyboardType: TextInputType.number,
inputFormatters: [
// 1. Allow digits and decimal
FilteringTextInputFormatter.allow(
RegExp(r'[0-9.]'),
),
// 2. Currency formatting
CurrencyInputFormatter(),
// 3. Limit total length
LengthLimitingTextInputFormatter(12),
],
),
],
),
),
);
}
}
What's happening here? - Combining allow and length limit - Auto-capitalization with formatters - Multiple filters for price input - Formatters execute in order
Custom TextInputFormatter
Creating custom formatters.
/// Custom formatter for specific patterns
class SSNInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Remove all non-digits
final text = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');
if (text.isEmpty) {
return newValue.copyWith(text: '');
}
// Format as XXX-XX-XXXX
final buffer = StringBuffer();
for (int i = 0; i < text.length && i < 9; i++) {
if (i == 3 || i == 5) {
buffer.write('-');
}
buffer.write(text[i]);
}
return newValue.copyWith(
text: buffer.toString(),
selection: TextSelection.collapsed(offset: buffer.length),
);
}
}
/// Custom formatter with validation
class ValidatingFormatter extends TextInputFormatter {
final String pattern;
final String errorMessage;
ValidatingFormatter(this.pattern, this.errorMessage);
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Check if the new value matches the pattern
if (newValue.text.isNotEmpty &&
!RegExp(pattern).hasMatch(newValue.text)) {
// Revert to old value if invalid
return oldValue;
}
return newValue;
}
}
/// Custom formatter for hexadecimal input
class HexInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Allow only hex characters
final filtered = newValue.text.replaceAll(
RegExp(r'[^0-9a-fA-F]'),
'',
);
// Auto-uppercase
final formatted = filtered.toUpperCase();
return newValue.copyWith(
text: formatted,
selection: TextSelection.collapsed(offset: formatted.length),
);
}
}
/// Using custom formatters
class CustomTextInputFormatterExample extends StatefulWidget {
const CustomTextInputFormatterExample({super.key});
@override
State<CustomTextInputFormatterExample> createState() => _CustomTextInputFormatterExampleState();
}
class _CustomTextInputFormatterExampleState extends State<CustomTextInputFormatterExample> {
final TextEditingController _ssnController = TextEditingController();
final TextEditingController _hexController = TextEditingController();
final TextEditingController _validatedController = TextEditingController();
@override
void dispose() {
_ssnController.dispose();
_hexController.dispose();
_validatedController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Formatters'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 1. SSN formatter
TextField(
controller: _ssnController,
decoration: const InputDecoration(
labelText: 'SSN',
hintText: 'XXX-XX-XXXX',
border: OutlineInputBorder(),
helperText: 'Format: XXX-XX-XXXX',
),
keyboardType: TextInputType.number,
inputFormatters: [
SSNInputFormatter(),
LengthLimitingTextInputFormatter(11),
],
),
const SizedBox(height: 16),
// 2. Hex formatter
TextField(
controller: _hexController,
decoration: const InputDecoration(
labelText: 'Hex Color',
hintText: 'A1B2C3',
border: OutlineInputBorder(),
helperText: 'Hex characters only (0-9, A-F)',
),
inputFormatters: [
HexInputFormatter(),
LengthLimitingTextInputFormatter(6),
],
),
const SizedBox(height: 16),
// 3. Validating formatter
TextField(
controller: _validatedController,
decoration: const InputDecoration(
labelText: 'Postal Code',
hintText: '12345 or 12345-6789',
border: OutlineInputBorder(),
helperText: 'US Postal Code format',
),
inputFormatters: [
// Only allow digits and dash
FilteringTextInputFormatter.allow(
RegExp(r'[0-9\-]'),
),
// Format: 12345 or 12345-6789
ValidatingFormatter(
r'^\d{5}(-\d{4})?$',
'Invalid postal code format',
),
LengthLimitingTextInputFormatter(10),
],
),
],
),
),
);
}
}
What's happening here? - SSN format XXX-XX-XXXX - Hexadecimal auto-uppercase - Validating formatter for postal codes - Custom formatting logic
Best Practices
Use Appropriate Formatters
// Good - Correct formatter for the input
TextField(
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: InputDecoration(labelText: 'Age'),
)
// Bad - Wrong formatter
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z]')),
],
decoration: InputDecoration(labelText: 'Age'),
)
Combine Formatters
// Good - Multiple formatters
TextField(
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
LengthLimitingTextInputFormatter(10),
],
)
// Bad - One formatter for everything
TextField(
inputFormatters: [
// Trying to do everything in one formatter
],
)
Test Formatters
// Good - Test custom formatters
test('Phone formatter formats correctly', () {
final formatter = PhoneInputFormatter();
// Test various inputs
expect(formatter.formatEditUpdate(...), ...);
});
Common Mistakes
Not Considering Keyboard Type
Wrong:
// Text keyboard with number formatter
TextField(
keyboardType: TextInputType.text,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
Correct:
// Number keyboard with number formatter
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
Formatters in Wrong Order
Wrong:
// Length limit before formatting
inputFormatters: [
LengthLimitingTextInputFormatter(14),
PhoneInputFormatter(),
]
Correct:
// Formatting before length limit
inputFormatters: [
PhoneInputFormatter(),
LengthLimitingTextInputFormatter(14),
]
Summary
Input Formatters control and format user input as they type. Use filtering formatters for character restrictions, length limiters for max characters, and custom formatters for specific formats like phone numbers, credit cards, and dates. Combine formatters for complex formatting requirements.
Next Steps
Did You Know?
- Formatters process each keystroke
- Formatters execute in order
- Custom formatters can implement any logic
- Formatters can auto-format input
- Formatters can validate input
- Formatters work with any TextField
- Formatters can be combined
- Formatters improve user experience