
📱 Phone input mask in JavaScript: 4 methods with code
Users enter phone numbers in all sorts of ways: some start with 8, some with +7, some type digits without spaces, others add spaces after every pair of digits. The database ends up with a mess. Managers waste time making calls, and half the numbers turn out to be broken. A familiar scenario for anyone who has ever added a contact form to a website.
An input mask solves this problem completely: the user physically cannot enter a number in the wrong format. They type digits, and those digits automatically appear in the right places while brackets and hyphens show up on their own. No errors, no frustration.
Below are four ways to add a phone mask to an input: from native HTML5 with zero JavaScript to a full-featured library with flags for every country. With code you can copy and paste into your project.
💡 Quick overview:
- The simplest mask uses the
patternattribute andtype="tel", no JavaScript at all, works in any browser - Need country flags and international formatting, use
intl-tel-input, the most popular solution on GitHub - Need one library for phones, dates, cards, and any other masks, go with
IMask.js - Want to write a mask by hand without pulling in dependencies, plain JavaScript can do it in 30 lines
What is an input mask and why it matters more than validation
An input mask is a template string that controls what the user sees and can type in a form field. Unlike validation, which checks data after submission and shows an error, a mask works in real time during input: it prevents typing a letter where a digit should be and automatically inserts brackets, hyphens, and spaces.
The difference is fundamental. Validation says "you made a mistake," and the user is already frustrated and might leave. A mask guides them step by step: errors are simply impossible. For phone numbers this is critical because there are dozens of formatting options. A Russian number alone can be written as +7 999 123-45-67, as 8(999)1234567, or as +7(999)123-45-67, and these are just three variants out of many. Without a mask, every user invents their own format, and the database turns into a junk pile.
Method 1: HTML5, type="tel" and pattern
The easiest path: skip JavaScript entirely. Modern browsers support type="tel" and the pattern attribute, which validates input against a regular expression on form submission.
Add the field to your markup. The code can be inserted into any HTML template: Contact Form 7, a native WordPress form, or custom markup.
1 <input 2 type="tel" 3 id="phone" 4 name="phone" 5 pattern="\+7\s?[\(]?\d{3}[\)]?\s?\d{3}[\-]?\d{2}[\-]?\d{2}" 6 placeholder="+7 (999) 123-45-67" 7 title="Enter the number in the format +7 (999) 123-45-67" 8 required 9 >
Let's break down what's happening here. type="tel" displays a numeric keypad on mobile devices instead of the regular keyboard, making phone entry more convenient. pattern sets a regular expression that allows several variants: with or without spaces, with or without brackets around the area code. title shows a tooltip on hover, and required prevents submitting the form with an empty field.
The advantage: zero dependencies, works in any browser. For a simple landing page, this is enough.
But this is not a true mask. Brackets and hyphens do not appear automatically; the user must type them manually. pattern only triggers on form submission. If you need formatting in real time during input, move on to the next methods.
Method 2: intl-tel-input, international format with country flags
intl-tel-input is the most popular JavaScript library for phone input. It provides a field with a dropdown list of country flags, auto-formatting to match the national template, and number validation. As of June 2026, the library has over 7,800 stars on GitHub, and the current version is 29.
Install the library via npm (npm i intl-tel-input) or CDN. The code below is for vanilla JavaScript; there are also ready-made components for React, Vue, Angular, and Svelte.
1 <!-- In the <head> of the page --> 2 <link 3 rel="stylesheet" 4 href="https://cdn.jsdelivr.net/npm/intl-tel-input@29/build/css/intlTelInput.css" 5 > 6 <script src="https://cdn.jsdelivr.net/npm/intl-tel-input@29/build/js/intlTelInput.min.js"></script>
The input element and initialization go in the template where you need the field. For a WordPress theme, add the script via wp_enqueue_script in functions.php, and place the markup in the appropriate template.
1 <input type="tel" id="phone"> 2 3 <script> 4 const input = document.querySelector("#phone"); 5 const iti = intlTelInput(input, { 6 initialCountry: "ru", 7 separateDialCode: true, 8 utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@29/build/js/utils.js" 9 }); 10 </script>
initialCountry: "ru" sets the Russian flag by default. separateDialCode: true moves the country code outside the input field: when the user changes the flag, the code is inserted automatically. utilsScript loads formatting utilities; without them the library still works, but numbers are not formatted to match the selected country's pattern.
The object returned by intlTelInput() provides several useful methods. iti.getNumber() returns the number in E.164 international format, iti.isValidNumber() checks validity, and iti.setNumber("9991234567") sets the value programmatically. For Contact Form 7 you will need a custom script: find the .wpcf7-tel field and attach intlTelInput to it.
Advantage: a complete international solution with auto-formatting, flags, validation, and framework integrations. Disadvantage: the library weighs around 40 KB (gzip), which is overkill for a simple landing page with one field.
Method 3: IMask.js, a universal mask without jQuery
IMask.js is a mature masking library written in plain JavaScript: no jQuery, no other dependencies. It supports masks for phones, dates, numbers, currencies, and dynamic templates. It comes in handy when you need different types of masks on one site: a phone in a contact form, a date in a calendar, and an amount in a calculator. The current version is 7.1.
Install via npm (npm i imask) or include the CDN:
1 <script src="https://unpkg.com/imask"></script>
A phone mask in three lines. The code can go in a <script> tag in the footer or in a separate JS file in your theme.
1 const phoneInput = document.getElementById('phone'); 2 const maskOptions = { 3 mask: '+{7} (000) 000-00-00' 4 }; 5 IMask(phoneInput, maskOptions);
Mask syntax: +{7} is a fixed prefix that the user cannot edit. 0 stands for any digit from 0 to 9. You can add complexity: make the area code optional or dynamically switch the mask when the country changes using the dispatch parameter.
IMask returns an object with access to the raw value: maskRef.unmaskedValue will give you plain digits 79991234567, exactly what you want to save to the database. The method maskRef.value returns the formatted string for display. The classic problem is solved: digits go to the backend, while the user sees a nicely formatted number.
Advantage: one library for all cases, raw values out of the box, no dependencies. Disadvantage: for a single phone field it is easier to use intl-tel-input with flags; IMask really shines on forms with three to five different field types.
Method 4: plain JavaScript, a handmade mask
If you do not want to pull in a library for just one field, write the mask in plain JavaScript. The code below handles every keystroke and automatically inserts brackets and hyphens as digits are entered.
Place the script in the same template where the input field is located, or include it as a separate file. For WordPress, use wp_enqueue_script with the true flag (in the footer).
1 document.getElementById('phone').addEventListener('input', function (e) { 2 let value = e.target.value.replace(/\D/g, ''); 3 4 if (value.length > 11) { 5 value = value.slice(0, 11); 6 } 7 8 let formatted = '+7 ('; 9 if (value.length > 1) formatted += value.slice(1, 4); 10 if (value.length > 4) formatted += ') ' + value.slice(4, 7); 11 if (value.length > 7) formatted += '-' + value.slice(7, 9); 12 if (value.length > 9) formatted += '-' + value.slice(9, 11); 13 14 e.target.value = formatted; 15 });
The input event fires on every field change: pasting from the clipboard, autocorrect, deletion. The first line replace(/\D/g, '') strips everything except digits: letters and special characters disappear instantly. Then the script trims extra digits (a Russian number is 11 digits including the country code) and builds the formatted string: country code +7, three-digit area code in brackets, three digits, two, two, separated by hyphens.
Before using this on a live site, add two improvements. First: when deleting digits in the middle of the number, the cursor may jump to the end of the field; save the caret position via selectionStart and restore it after formatting. Second: for numbers from other countries (Belarus has 12 digits, Kazakhstan has 11 digits with a different code) the logic gets more complex, and at that point intl-tel-input is fully justified.
Advantage: 30 lines, full control, zero kilobytes of dependencies. Disadvantage: the mask is tailored to one format; for multi-country projects the code will bloat.
⁉️🤔 Frequently asked questions
Why is a mask better than simple validation?
Validation triggers after form submission: the user sees an error and has to go back and fix it. A mask works during input, making an invalid format physically impossible. Less frustration, fewer abandoned forms. For phone numbers this is critical: errors in the number are one of the most common reasons users abandon a form.
How do I save plain digits to the database without brackets and hyphens?
All the libraries in this article provide raw values:
intl-tel-inputhas agetNumber()method that returns+79991234567,IMask.jshas anunmaskedValueproperty that returns79991234567. Before sending data to the server, grab the raw value and save it. Show the formatted string only in the interface. In WordPress forms, add a hidden field<input type="hidden" name="phone_raw">and populate it via JavaScript before submission.
Does the mask work on mobile devices?
Yes, all four methods work correctly on iOS and Android.
type="tel"brings up the numeric keypad, which is convenient. On older Android (versions below 6) theinputevent may behave differently with autocorrect; test on real devices.intl-tel-inputandIMask.jsare tested by their developers on mobile browsers and cause no issues.
Can I use a mask with Contact Form 7?
CF7 generates fields with
.wpcf7-telclasses. Write a script that finds this field by selector and attachesintl-tel-inputorIMaskto it. On form submission CF7 takes the value directly from the field, so the library must have time to format it beforehand. If you use raw values, intercept thewpcf7submitevent and replace the data in a hidden field.
What should I do with numbers from different countries on the same site?
Use
intl-tel-input: the user selects a flag, and the mask automatically switches to match the format of the chosen country. For geotargeting you can setinitialCountry: "auto", and the library will detect the country by IP via an external service. If there are only two or three countries and your dependency budget is minimal, write two masks in plain JavaScript and switch between them with a "Russia / Kazakhstan" radio button above the field.
Which method to choose for your task
The choice boils down to three questions: how many fields are on the site, do you need country flags, and how critical is bundle size.
A single simple form on a landing page: start with HTML5 pattern. Fast, no JavaScript, and on mobile the keyboard switches to numeric. If the site accepts international numbers, intl-tel-input will cover everything: flags, auto-formatting, validation. For an online store or SaaS this is the standard.
Several different masks on the site (phone, date, amount, card): IMask.js will give you one API for all fields and does not require jQuery. Building a pet project and want full control: 30 lines of plain JavaScript will handle Russian numbers without a single dependency.
Test how the mask works on a live site: enter a number with letters, paste from the clipboard, try deleting a couple of digits in the middle. The field should behave predictably in any scenario. Which method did you choose for your project? Share in the comments.



