React: How to Format Phone Numbers in Real-Time as Users Type
Phone numbers are a critical input in many applications—from user registration to checkout flows. However, unformatted phone numbers (e.g., 1234567890) are hard to read and prone to errors. Real-time formatting (e.g., (123) 456-7890) enhances user experience by making inputs more readable, reduces data entry mistakes, and ensures consistency in your database.
In this blog, we’ll explore how to implement real-time phone number formatting in React. We’ll cover custom regex-based solutions for simple formats, library-based approaches for complex cases (like international numbers), best practices, and troubleshooting common issues. By the end, you’ll be able to format phone numbers seamlessly as users type.
Table of Contents#
- Why Real-Time Formatting Matters
- Common Phone Number Formats
- Approaches to Formatting in React
- Step-by-Step: Regex-Based Formatting
- Libraries for Simplified Formatting
- Best Practices
- Troubleshooting Common Issues
- Conclusion
- References
Why Real-Time Formatting Matters#
- User Experience (UX): Formatted numbers are easier to read and verify, reducing frustration during input.
- Data Consistency: Ensures all phone numbers in your database follow a uniform structure, simplifying storage and retrieval.
- Error Reduction: Real-time feedback helps users catch typos early (e.g., missing digits).
- Professionalism: Polished inputs signal attention to detail, building trust with users.
Common Phone Number Formats#
Formats vary by region, but some popular examples include:
- US/Canada:
(XXX) XXX-XXXX(e.g.,(123) 456-7890) - UK:
+44 (XXX) XXX-XXXX(country code + local format) - International:
+XX (XXX) XXX-XXXX(with country code, e.g.,+1 (123) 456-7890for US).
Approaches to Formatting in React#
React’s state-driven architecture makes real-time formatting straightforward. You’ll either:
- Use regex for simple, custom formats (e.g., US local numbers).
- Leverage libraries for complex cases (e.g., international numbers with country codes).
Step-by-Step: Regex-Based Formatting#
For simple formats (e.g., US (XXX) XXX-XXXX), regex is lightweight and customizable. Let’s build a component that formats numbers as users type.
1. Set Up the Component#
Start with a basic React component using useState to track the phone number:
import { useState } from 'react';
const PhoneInput = () => {
const [phoneNumber, setPhoneNumber] = useState('');
const handleChange = (e) => {
// Format logic will go here
};
return (
<div>
<label htmlFor="phone">Phone Number:</label>
<input
type="text"
id="phone"
value={phoneNumber}
onChange={handleChange}
placeholder="(123) 456-7890"
/>
</div>
);
};
export default PhoneInput;2. Write the Formatting Logic#
The handleChange function will:
- Remove non-digit characters (e.g., letters, symbols).
- Apply the US format using regex.
- Update the state with the formatted value.
Code:#
const handleChange = (e) => {
// Step 1: Remove non-digit characters
const rawInput = e.target.value.replace(/\D/g, ''); // \D matches non-digits
// Step 2: Apply US format (XXX) XXX-XXXX
let formatted = '';
if (rawInput.length > 0) {
formatted = `(${rawInput.substring(0, 3)}`; // (123
}
if (rawInput.length >= 3) {
formatted += `) ${rawInput.substring(3, 6)}`; // ) 456
}
if (rawInput.length >= 6) {
formatted += `-${rawInput.substring(6, 10)}`; // -7890 (truncate to 10 digits)
}
// Update state with formatted value
setPhoneNumber(formatted);
};3. Handle Edge Cases#
- Backspacing: If the user deletes digits, the format should adjust (e.g., deleting from
(123) 456-7890to(123) 456should remove the-). - Pasting Numbers: Users may paste unformatted numbers (e.g.,
1234567890). The regexreplace(/\D/g, '')already strips non-digits, so pasted input will format correctly.
4. Full Component with Regex#
import { useState } from 'react';
const USPhoneInput = () => {
const [phoneNumber, setPhoneNumber] = useState('');
const handleChange = (e) => {
const rawInput = e.target.value.replace(/\D/g, ''); // Keep only digits
let formatted = '';
// Apply (XXX) XXX-XXXX mask
if (rawInput.length > 0) {
formatted = `(${rawInput.slice(0, 3)}`;
}
if (rawInput.length >= 3) {
formatted += `) ${rawInput.slice(3, 6)}`;
}
if (rawInput.length >= 6) {
formatted += `-${rawInput.slice(6, 10)}`; // Limit to 10 digits
}
setPhoneNumber(formatted);
};
return (
<div>
<label htmlFor="us-phone">US Phone Number:</label>
<input
type="text"
id="us-phone"
value={phoneNumber}
onChange={handleChange}
placeholder="(123) 456-7890"
maxLength={14} // (XXX) XXX-XXXX = 14 characters
/>
</div>
);
};
export default USPhoneInput;Libraries for Simplified Formatting#
For complex formats (e.g., international numbers, dynamic country codes), libraries handle edge cases like cursor positioning and locale support. Let’s explore two popular options.
react-input-mask for Basic Masks#
react-input-mask is a lightweight library that enforces input masks with minimal code. It handles cursor jumping (a common regex issue) automatically.
Step 1: Install the Library#
npm install react-input-mask --save
# or
yarn add react-input-maskStep 2: Use the Mask Component#
Import InputMask and define your format with a mask string (e.g., (999) 999-9999 for US numbers, where 9 = digit):
import InputMask from 'react-input-mask';
import { useState } from 'react';
const MaskedPhoneInput = () => {
const [phoneNumber, setPhoneNumber] = useState('');
return (
<div>
<label htmlFor="masked-phone">Masked Phone Number:</label>
<InputMask
mask="(999) 999-9999" // US format mask
maskChar=" " // Use space for empty digits (optional)
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="(123) 456-7890"
id="masked-phone"
/>
</div>
);
};
export default MaskedPhoneInput;Why this works: react-input-mask enforces the mask in real time, and the cursor stays in the correct position when editing.
International Numbers with intl-tel-input#
For global apps, @formatjs/intl-tel-input (or react-intl-tel-input) supports country codes, validation, and locale-specific formatting.
Step 1: Install Dependencies#
npm install @formatjs/intl-tel-input react-intl-tel-input --saveStep 2: Basic International Input#
This component includes a country code dropdown and auto-formats based on the selected country:
import { useState } from 'react';
import IntlTelInput from 'react-intl-tel-input';
import 'react-intl-tel-input/dist/main.css'; // Import styles
const InternationalPhoneInput = () => {
const [phoneNumber, setPhoneNumber] = useState('');
const handleChange = (value) => {
setPhoneNumber(value); // Value includes country code (e.g., +11234567890)
};
return (
<div>
<label htmlFor="international-phone">International Phone:</label>
<IntlTelInput
value={phoneNumber}
onChange={handleChange}
defaultCountry="us" // Default to US
placeholder="Enter phone number"
/>
</div>
);
};
export default InternationalPhoneInput;Features:
- Country code dropdown with flags.
- Validation (e.g., checks if the number is valid for the selected country).
- Auto-formatting based on locale.
Best Practices#
-
Accessibility:
- Use
<label htmlFor="input-id">to associate labels with inputs. - Add
aria-describedbyfor hints (e.g., "Format: (123) 456-7890").
- Use
-
Validation:
- After formatting, validate the number (e.g., check length: US numbers need 10 digits).
- Example validation for US numbers:
const isUSPhoneValid = (formattedNumber) => { const digits = formattedNumber.replace(/\D/g, ''); return digits.length === 10; // 10 digits = valid US number };
-
Localization:
- For global apps, use libraries like
intl-tel-inputto support region-specific formats.
- For global apps, use libraries like
-
Testing:
- Test edge cases: pasting numbers, deleting digits, and entering non-digit characters.
Troubleshooting Common Issues#
- Cursor Jumping: Regex-based formatting may cause the cursor to reset. Use libraries like
react-input-maskto avoid this. - Invalid Country Codes: For international inputs, validate country codes with
intl-tel-input’s built-in methods. - Pasted Text: Ensure pasted input (e.g.,
123-456-7890) is stripped of non-digits before formatting (handled by regex or libraries).
Conclusion#
Real-time phone number formatting in React improves UX and data consistency. For simple formats, regex is lightweight and customizable. For complex cases (international numbers, cursor management), libraries like react-input-mask or intl-tel-input save time. Always prioritize accessibility, validation, and testing to ensure a smooth user experience.