Ever watched a user try to type a phone number into your app, only to watch them struggle with dashes, spaces, and accidental letters? It happens all the time. Bad text input breaks user experience fast.
When you build forms in Flutter, raw text fields are rarely enough. You need precise control over what your users can type and how that data displays on their screen while they type. That is where a Flutter input formatter comes in.
Whether you need a Flutter textfield formatter to format currency in real time, restrict a field to allow only numbers, or force uppercase for coupon codes, Flutter makes it surprisingly easy once you know the right approach.
In this guide, we will dive deep into TextInputFormatter. You will learn how to handle numbers, currency, phone numbers, credit cards, and custom regex rules so your app feels smooth, polished, and production-ready.
InputFormatter Basics
Before we dive into specific formatting tricks, let's understand how a Flutter input formatter works under the hood.
In Flutter, when a user types into a TextField, the inputFormatters parameter intercepts that raw text before it reaches the field's state. It receives two values:
-
oldValue: What was in the text field before the keystroke. -
newValue: What the text field would contain after the keystroke.
The formatter inspects newValue, cleans or formats it, and returns a new TextEditingValue.
If the user types an illegal character, the formatter simply returns oldValue, preventing the unwanted character from ever appearing on screen!
Flutter gives us built-in formatters through the services library:
import 'package:flutter/services.dart';
Basic TextField with Formatters
Here is a full, working Flutter example using your boilerplate to show how formatters attach to a TextField:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Input Formatter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.blue,
brightness: Brightness.light,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('InputFormatter Basics')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Basic InputFormatter Example'),
const SizedBox(height: 12),
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Enter Text',
hintText: 'Type here...',
border: OutlineInputBorder(),
),
// Pass formatters in a list to flutter textfield formatter
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]')),
],
),
],
),
),
);
}
}
In this setup, FilteringTextInputFormatter.allow stops users from typing digits or symbols into the field.
Take Your Flutter Skills to the Next Level
Build professional Flutter apps through structured lessons, practical projects, and a complete capstone.
Number-Only Fields
One of the most common user inputs is a numeric value: age, quantity, PIN, or identification numbers. To ensure clean data, you must configure your field to allow only numbers.
In Flutter, you can restrict input to digits in two ways:
- Setting the keyboard type using
keyboardType: TextInputType.number. - Restricting keypresses using
FilteringTextInputFormatter.digitsOnlyorFilteringTextInputFormatter.allow(RegExp(r'[0-9]')).
Important Note: Always set both! Changing the keyboard type presents a numeric keypad to mobile users, but users can still paste text or use desktop keyboards. Adding a flutter input formatter number only rule guarantees that only valid digits pass through.
Working Example: Digits Only Field
Here is a runnable example using your boilerplate to lock down a flutter textfield number format to digits only:
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _numberController = TextEditingController();
@override
void dispose() {
_numberController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Number-Only Input')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Quantity or Age'),
const SizedBox(height: 12),
TextField(
controller: _numberController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Numbers Only',
hintText: 'e.g., 25',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.numbers),
),
inputFormatters: [
// Allows only digits 0-9 and blocks all other characters/symbols
FilteringTextInputFormatter.digitsOnly,
],
),
],
),
),
);
}
}
Now, whether the user types from a physical keyboard or pastes text with letters, Flutter instantly drops non-digit characters!
Currency Formatter
Formatting money accurately while a user types is a key requirement for ecommerce, banking, and budget tracking apps.
A smooth flutter currency format handles comma separators, decimal places, and currency symbols live as keys are pressed.
To build a reliable flutter input formatter currency solution, we can combine Flutter's custom TextInputFormatter with the intl package's NumberFormat.currency.
Working Example: Real-Time Currency Formatting
Here is a runnable example using your boilerplate that formats currency as the user types:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
intl: ^0.20.3
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _currencyController = TextEditingController();
@override
void dispose() {
_currencyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Currency Formatter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Payment Amount'),
const SizedBox(height: 12),
TextField(
controller: _currencyController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Amount',
hintText: '0.00',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.attach_money),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
CurrencyInputFormatter(),
],
),
],
),
),
);
}
}
/// Custom formatter that formats digits into a USD currency string.
class CurrencyInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) {
return newValue.copyWith(text: '');
}
// Convert raw digits to integer cents
final double value = double.parse(newValue.text) / 100;
final NumberFormat formatter = NumberFormat.currency(
locale: 'en_US',
symbol: '\$',
decimalDigits: 2,
);
final String newText = formatter.format(value);
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: newText.length),
);
}
}
Key Takeaways for Currency Input
-
Cent-Based Typing: The formatter turns typed numbers into cents first (e.g., typing
1,2,5becomes$1.25). This keeps decimal formatting effortless without fighting raw cursor positions. -
Selection Management: Notice how
TextSelection.collapsed(offset: newText.length)keeps the cursor at the end of the formatted text after each keystroke.
Phone Number Formatting
Phone numbers can be frustrating to type if users have to manually add parentheses, spaces, or dashes.
Implementing a clean flutter phone number format improves accuracy and makes your onboarding or checkout flows feel much smoother.
Using a custom flutter input formatter, you can transform a raw stream of digits into a standard format—like (123) 456-7890—automatically as the user types.
Working Example: Auto-Formatting US Phone Numbers
Here is a full working example using your boilerplate to dynamically format phone numbers on the fly:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _phoneController = TextEditingController();
@override
void dispose() {
_phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Phone Number Formatter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Phone Number'),
const SizedBox(height: 12),
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone Number',
hintText: '(123) 456-7890',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
PhoneNumberInputFormatter(),
],
),
],
),
),
);
}
}
/// Formats a 10-digit raw phone string into (XXX) XXX-XXXX format.
class PhoneNumberInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final String text = newValue.text;
if (text.isEmpty) {
return newValue;
}
final StringBuffer buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
if (i == 0) buffer.write('(');
if (i == 3) buffer.write(') ');
if (i == 6) buffer.write('-');
buffer.write(text[i]);
}
final String formattedText = buffer.toString();
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: formattedText.length),
);
}
}
How This Works
-
FilteringTextInputFormatter.digitsOnlyensures non-numeric characters are rejected upfront. -
LengthLimitingTextInputFormatter(10)caps raw digit entry to 10 digits before formatting. -
PhoneNumberInputFormattersteps through digits and injects formatting symbols at indexes0,3, and6.
Credit Card Formatting
Processing payments inside a mobile app requires maximum clarity. When users enter payment details, grouped numbers reduce typing mistakes and build user trust.
With a dedicated flutter textfield formatter, you can automatically split credit card digits into clean, four-digit blocks (like XXXX XXXX XXXX XXXX) live as the user types.
Combining FilteringTextInputFormatter.digitsOnly, a LengthLimitingTextInputFormatter, and a custom flutter input formatter ensures your payment field strictly accepts 16 digits while presenting them in a standard, easily readable credit card format.
Working Example: Real-Time Credit Card Masking
Here is a full, runnable example using your boilerplate to handle credit card input formatting in Flutter:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _cardController = TextEditingController();
@override
void dispose() {
_cardController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Credit Card Formatter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Credit Card Number'),
const SizedBox(height: 12),
TextField(
controller: _cardController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Card Number',
hintText: '4532 0123 4567 8901',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.credit_card),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(16),
CreditCardInputFormatter(),
],
),
],
),
),
);
}
}
/// Custom formatter that adds a space after every 4 digits.
class CreditCardInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final String text = newValue.text;
if (text.isEmpty) {
return newValue;
}
final StringBuffer buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
buffer.write(text[i]);
// Add space every 4 digits, but not at the very end
final int nonSpaceIndex = i + 1;
if (nonSpaceIndex % 4 == 0 && nonSpaceIndex != text.length) {
buffer.write(' ');
}
}
final String formattedText = buffer.toString();
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: formattedText.length),
);
}
}
Why This Design Works
-
Pre-Filtering:
FilteringTextInputFormatter.digitsOnlyensures letters and special symbols never reach our custom class. -
Length Guard:
LengthLimitingTextInputFormatter(16)keeps raw numeric input capped at 16 digits, preventing oversized card inputs. - Auto-Spacing: The loop dynamically inserts spaces at index intervals of 4, keeping the user experience seamless across all device sizes.
Uppercase Conversion
Certain form fields—such as promo codes, state abbreviations, flight record locators (PNR), and vehicle identification numbers (VIN)—require input in all capital letters.
Relying on users to manually tap the shift key on their mobile keyboard leads to inconsistent data and accidental errors.
With a simple custom flutter textfield formatter, you can easily force uppercase input in real time. As the user types lowercase letters, your flutter input formatter transforms them instantly before they appear inside the field.
Working Example: Force Uppercase Input
Here is a runnable example using your boilerplate that automatically converts typed text to capital letters:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _promoController = TextEditingController();
@override
void dispose() {
_promoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Uppercase Formatter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Coupon or Promo Code'),
const SizedBox(height: 12),
TextField(
controller: _promoController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
labelText: 'Promo Code',
hintText: 'SAVE50OFF',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.confirmation_number),
),
inputFormatters: [
// Enforces uppercase formatting even during text pastes or software keyboard overrides
UpperCaseTextFormatter(),
],
),
],
),
),
);
}
}
/// Custom formatter that transforms every incoming character to uppercase.
class UpperCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}
Pro Tip: textCapitalization vs. TextInputFormatter
Flutter has a built-in property called textCapitalization: TextCapitalization.characters. While this opens the mobile keyboard in capital mode, it has two drawbacks:
- It is a suggestion to the mobile OS—some soft keyboards ignore it.
- It does not work on pasted text or desktop hardware keyboards.
Combining textCapitalization with our custom UpperCaseTextFormatter guarantees that your flutter inputformatters setup works consistently across all platforms and input methods!
Character Restrictions
Preventing users from typing illegal characters or exceeding length boundaries is crucial for keeping database records clean.
Whether you are building a username field, a postal code input, or a max-length bio field, character restriction rules save you from handling dirty data later.
In Flutter, character restrictions generally fall into two categories:
- Length Restrictions: Capping the maximum total characters allowed in a field.
- Deny/Allow Rules: Blocking specific unwanted symbols or permitting only a specific set of allowed characters.
Using built-in flutter inputformatters like LengthLimitingTextInputFormatter and FilteringTextInputFormatter.deny, you can effortlessly enforce these limits.
Working Example: Username Field with Length and Character Limits
Here is a full, runnable example using your boilerplate that limits a username to 12 characters and completely blocks spaces and special symbols:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _usernameController = TextEditingController();
@override
void dispose() {
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Character Restrictions')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Create Username'),
const SizedBox(height: 12),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username (Max 12 chars)',
hintText: 'john_doe123',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
inputFormatters: [
// 1. Cap total characters at 12
LengthLimitingTextInputFormatter(12),
// 2. Deny spaces specifically
FilteringTextInputFormatter.deny(RegExp(r'\s')),
// 3. Allow only alphanumeric characters and underscores
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9_]')),
],
),
],
),
),
);
}
}
Key Formatting Tools to Remember
-
LengthLimitingTextInputFormatter(int max): Automatically stops keypresses once the text reaches your character cap. -
FilteringTextInputFormatter.deny(Pattern pattern): Rejects matching characters. For instance,FilteringTextInputFormatter.deny(RegExp(r'[#$%-]'))quickly shields your flutter textfield formatter from invalid characters! -
FilteringTextInputFormatter.allow(Pattern pattern): Serves as a strict allowlist. Anything outside your pattern gets discarded immediately.
Regex Formatters
Regular expressions (regex) give you complete, fine-grained control over what users can type into a field.
When pre-built formatters aren't enough, using a flutter regex pattern with FilteringTextInputFormatter lets you create flexible rules for complex text patterns.
Whether you need a flutter textfield formatter that accepts alphanumeric codes, permits specific special characters, or restricts input to hexadecimal color codes, regular expressions handle it effortlessly.
In Flutter, you can apply regex in two main ways:
-
FilteringTextInputFormatter.allow(RegExp(pattern)): Only permits characters that match the regex. -
FilteringTextInputFormatter.deny(RegExp(pattern)): Blocks any character that matches the regex.
Working Example: Hex Color Code Field
Here is a runnable example using your boilerplate that uses a flutter input formatter driven by regex to accept only valid hexadecimal color characters (digits 0-9 and letters A-F / a-f, along with an optional leading # symbol):
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _hexController = TextEditingController();
@override
void dispose() {
_hexController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Regex Formatters')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Hex Color Code'),
const SizedBox(height: 12),
TextField(
controller: _hexController,
decoration: const InputDecoration(
labelText: 'Hex Code (e.g. #FF5733)',
hintText: '#FF5733',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.color_lens),
),
inputFormatters: [
// 1. Cap total characters at 7 (# + 6 hex digits)
LengthLimitingTextInputFormatter(7),
// 2. Allow only hex characters (0-9, a-f, A-F) and the # symbol
FilteringTextInputFormatter.allow(RegExp(r'[#0-9a-fA-F]')),
],
),
],
),
),
);
}
}
Handy Regex Patterns for Flutter InputFormatters
Here are a few popular flutter inputformatters regex patterns you can copy into your apps:
-
Alphanumeric with spaces:
RegExp(r'[a-zA-Z0-9 ]') -
Decimal numbers (digits + single dot):
RegExp(r'^\d*\.?\d*') -
Only lowercase English letters:
RegExp(r'[a-z]') -
Remove all emoji/special symbols:
RegExp(r'[\w\s]')
Multiple Formatters
In real-world applications, a single input constraint is rarely enough. A promo code input might need both length limits and uppercase conversion. A credit card field requires number filtering, a character ceiling, and custom spacing.
Because the inputFormatters parameter takes a List<TextInputFormatter>, Flutter lets you stack multiple flutter inputformatters together in a clean pipeline.
How Flutter Executes Multiple Formatters
When you pass multiple formatters to a TextField, Flutter executes them sequentially, in the exact order they appear in the list:
User Input -> Formatter 1 -> Formatter 2 -> Formatter 3 -> Text Rendered
Pro Tip: Order matters! Always place strict filtering formatters (like
FilteringTextInputFormatter.digitsOnly) before layout formatters (like custom spacing or mask injectors). If you placedigitsOnlyafter a custom phone formatter that adds dashes, the digit filter will erase all the dashes your custom formatter just added!
Working Example: Combining 3 Formatters for Product Codes
Here is a full, runnable example using your boilerplate that combines three separate formatters:
-
FilteringTextInputFormatter.allow: Permits only letters and numbers (no special symbols). -
LengthLimitingTextInputFormatter: Limits total input length to 8 characters. -
UpperCaseTextFormatter: Forces all characters to uppercase in real time.
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _codeController = TextEditingController();
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multiple Formatters Example')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Product SKU'),
const SizedBox(height: 12),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: 'SKU Code (e.g., PROD1234)',
hintText: 'PROD1234',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.qr_code),
),
// Stacking multiple flutter textfield formatters in execution order
inputFormatters: [
// Step 1: Reject special characters or spaces
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
// Step 2: Cap max characters to 8
LengthLimitingTextInputFormatter(8),
// Step 3: Force uppercase transformation
UpperCaseTextFormatter(),
],
),
],
),
),
);
}
}
/// Custom formatter to turn text uppercase
class UpperCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}
Combining pre-built utilities with custom logic gives your flutter textfield formatter setup unmatched flexibility, ensuring dirty data never enters your controllers!
Creating Custom Formatters
While Flutter gives us handy built-in options like FilteringTextInputFormatter and LengthLimitingTextInputFormatter, production applications often require bespoke rules.
Whether you need a dynamic date mask (MM/YY), a specialized international postal code, or custom character grouping, creating a custom flutter input formatter gives you total control over user entry.
Anatomy of TextInputFormatter
To build a custom flutter textfield formatter, you extend TextInputFormatter and override the formatEditUpdate method:
class MyCustomFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Your custom formatting logic here
return newValue;
}
}
This method receives two parameters:
-
oldValue: The text, selection range, and state before the user's keystroke. -
newValue: The text, selection range, and state after the keystroke.
Your job is to inspect newValue, modify its text or cursor position if needed, and return a new TextEditingValue.
Working Example: Custom Date Formatter (MM/YY)
Here is a full, runnable example using your boilerplate that automatically builds an expiration date field (MM/YY) as the user types:
class _HomeScreenState extends State<HomeScreen> {
final TextEditingController _dateController = TextEditingController();
@override
void dispose() {
_dateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Date Formatter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter Expiration Date'),
const SizedBox(height: 12),
TextField(
controller: _dateController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Expiry Date',
hintText: 'MM/YY',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
CardExpirationFormatter(),
],
),
],
),
),
);
}
}
/// Custom Flutter input formatter that turns raw 4-digit input into MM/YY format.
class CardExpirationFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final String text = newValue.text;
if (text.isEmpty) {
return newValue;
}
final StringBuffer buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
buffer.write(text[i]);
// Add slash after the 2nd digit (month)
if (i == 1 && text.length > 2) {
buffer.write('/');
}
}
final String formattedText = buffer.toString();
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: formattedText.length),
);
}
}
Key Rules for Building Production-Grade Custom Formatters
- Always Handle Deletions Gracefully: Test backspaces! Make sure your formatter doesn't freeze or get stuck in an infinite loop when a user deletes characters.
-
Control Cursor Position (
TextSelection): Whenever you change the text length, update the cursor offset usingTextSelection.collapsed(offset: newText.length)so the cursor doesn't jump unexpectedly. -
Combine built-in formatters: Don't reinvent length limits or digit filtering inside your class. Use
FilteringTextInputFormatter.digitsOnlyorLengthLimitingTextInputFormatteralongside your custom class to keep code clean and maintainable.
Related Topics & Internal Guides
Mastering text input in Flutter involves more than just formatters. Check out these related guides to level up your forms:
- Form Validation Guide: Learn how to validate form inputs, display inline error messages, and block invalid form submissions.
-
TextField Mastery: Discover styling, custom decorations, controllers, and input actions for Flutter
TextFieldwidgets. - FocusNode Essentials: Control keyboard focus, shift focus automatically between fields, and build accessible keyboard navigation.
Ready to Build Professional Flutter Apps?
Turn today’s knowledge into real-world Flutter skills with Flutter Foundations.


Top comments (0)