DEV Community

Cover image for Flutter InputFormatters – Numbers, Currency, Phone Numbers and Custom Formatting
Flutter Sensei
Flutter Sensei

Posted on Originally published at fluttersensei.com

Flutter InputFormatters – Numbers, Currency, Phone Numbers and Custom Formatting

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:

  1. oldValue: What was in the text field before the keystroke.
  2. 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 ]')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Basic TextField with Formatters

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:

  1. Setting the keyboard type using keyboardType: TextInputType.number.
  2. Restricting keypresses using FilteringTextInputFormatter.digitsOnly or FilteringTextInputFormatter.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,
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Working Example: Digits Only Field

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, 5 becomes $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

  1. FilteringTextInputFormatter.digitsOnly ensures non-numeric characters are rejected upfront.
  2. LengthLimitingTextInputFormatter(10) caps raw digit entry to 10 digits before formatting.
  3. PhoneNumberInputFormatter steps through digits and injects formatting symbols at indexes 0, 3, and 6.

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.digitsOnly ensures 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:

  1. It is a suggestion to the mobile OS—some soft keyboards ignore it.
  2. 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:

  1. Length Restrictions: Capping the maximum total characters allowed in a field.
  2. 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:

  1. FilteringTextInputFormatter.allow(RegExp(pattern)): Only permits characters that match the regex.
  2. 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 place digitsOnly after 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:

  1. FilteringTextInputFormatter.allow: Permits only letters and numbers (no special symbols).
  2. LengthLimitingTextInputFormatter: Limits total input length to 8 characters.
  3. 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

  1. 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.
  2. Control Cursor Position (TextSelection): Whenever you change the text length, update the cursor offset using TextSelection.collapsed(offset: newText.length) so the cursor doesn't jump unexpectedly.
  3. Combine built-in formatters: Don't reinvent length limits or digit filtering inside your class. Use FilteringTextInputFormatter.digitsOnly or LengthLimitingTextInputFormatter alongside 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 TextField widgets.
  • 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)