Input Mask Fields — Auto-Format Inputs HTML CSS JS
Input Mask Fields · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="ims-card">
<h3>Formatted inputs</h3>
<div class="ims-field">
<label for="imsPhone">Phone number</label>
<input type="tel" id="imsPhone" data-mask="phone" inputmode="numeric" placeholder="(555) 123-4567">
</div>
<div class="ims-field">
<label for="imsCard">Card number</label>
<input type="text" id="imsCard" data-mask="card" inputmode="numeric" placeholder="1234 5678 9012 3456">
</div>
<div class="ims-row">
<div class="ims-field">
<label for="imsExp">Expiry</label>
<input type="text" id="imsExp" data-mask="expiry" inputmode="numeric" placeholder="MM/YY">
</div>
<div class="ims-field">
<label for="imsDate">Date</label>
<input type="text" id="imsDate" data-mask="date" inputmode="numeric" placeholder="DD/MM/YYYY">
</div>
</div>
<div class="ims-field">
<label for="imsCurrency">Amount</label>
<input type="text" id="imsCurrency" data-mask="currency" inputmode="decimal" placeholder="$0.00">
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:40px 24px}
.ims-card{background:#fff;border-radius:16px;padding:24px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.ims-card h3{font-size:17px;font-weight:800;color:#0f172a;margin-bottom:18px}
.ims-row{display:flex;gap:12px}
.ims-row .ims-field{flex:1}
.ims-field{margin-bottom:15px}
.ims-field label{display:block;font-size:12.5px;font-weight:700;color:#475569;margin-bottom:7px}
.ims-field input{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:11px 13px;font-size:14px;font-family:inherit;color:#0f172a;letter-spacing:.02em;font-variant-numeric:tabular-nums;transition:border-color .15s,box-shadow .15s}
.ims-field input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}// Each mask takes the raw typed value and returns the formatted display string.
var MASKS = {
phone: function (d) {
d = d.replace(/\D/g, '').slice(0, 10);
if (d.length > 6) return '(' + d.slice(0, 3) + ') ' + d.slice(3, 6) + '-' + d.slice(6);
if (d.length > 3) return '(' + d.slice(0, 3) + ') ' + d.slice(3);
if (d.length > 0) return '(' + d;
return '';
},
card: function (d) {
return d.replace(/\D/g, '').slice(0, 16).replace(/(.{4})/g, '$1 ').trim();
},
expiry: function (d) {
d = d.replace(/\D/g, '').slice(0, 4);
if (d.length > 2) return d.slice(0, 2) + '/' + d.slice(2);
return d;
},
date: function (d) {
d = d.replace(/\D/g, '').slice(0, 8);
var out = d.slice(0, 2);
if (d.length > 2) out += '/' + d.slice(2, 4);
if (d.length > 4) out += '/' + d.slice(4);
return out;
},
currency: function (raw) {
var d = raw.replace(/[^\d.]/g, '');
var parts = d.split('.');
var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
var dec = parts.length > 1 ? '.' + parts[1].slice(0, 2) : '';
return d === '' ? '' : '$' + intPart + dec;
},
};
document.querySelectorAll('[data-mask]').forEach(function (input) {
var fn = MASKS[input.dataset.mask];
input.addEventListener('input', function () {
// Preserve caret position relative to the end so editing mid-string feels natural.
var atEnd = this.selectionStart === this.value.length;
var formatted = fn(this.value);
this.value = formatted;
if (!atEnd) {
// Re-place the caret near where it was (best-effort for digit insertions).
var pos = Math.min(this.selectionStart, formatted.length);
this.setSelectionRange(pos, pos);
}
});
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Input Mask Fields — Live Auto-Formatting for Phone, Card, Date & Currency

An input mask formats a value as the user types — adding the parentheses and dashes to a phone number, the spaces to a card number, the slashes to a date — so the field is always readable and the user never has to type punctuation. This snippet builds a reusable masking system in plain HTML, CSS, and vanilla JavaScript, with five ready-made masks (phone, card number, expiry, date, currency) driven by a single data-mask attribute.
One attribute, any mask
Every masked input just declares data-mask="phone" (or card, expiry, date, currency), and a single setup loop wires the matching formatter to its input event. The masks live in a MASKS object where each entry is a pure function: it takes the raw typed value and returns the formatted display string. This keeps the system extensible — adding a new mask (SSN, IBAN, license plate) is one function in the object plus the attribute on the field, with no per-field code.
Format from digits, not from keystrokes
The robust way to mask is to strip the value down to its raw characters and re-build the formatted string from scratch on every input — rather than trying to insert punctuation at the caret per keystroke. The phone mask, for example, removes all non-digits, caps at 10, then assembles (555) 123-4567 from whatever digits remain. This "extract then reformat" approach handles every editing case correctly: typing, pasting a pre-formatted number, deleting from the middle, or selecting and replacing — because the output is always derived from the clean underlying value, not from the messy intermediate state.
The five built-in masks
*Phone* builds US-style (area) prefix-line progressively as digits arrive. *Card number* groups 16 digits into four space-separated blocks (the standard for readability and matching the printed card). *Expiry* inserts the MM/YY slash after two digits. *Date* assembles DD/MM/YYYY with slashes. *Currency* strips to digits and a decimal point, adds thousands separators with a regex, caps the decimals at two, and prefixes $. Each caps its length so the user can't overflow the format. These cover the inputs that most need masking — the ones where unformatted entry is hardest to read and most error-prone.
Caret handling
Re-rendering the whole value on every keystroke risks throwing the caret to the end mid-edit. The snippet preserves the end-of-string case (the common one — typing forward) naturally, and best-effort restores the caret position when editing in the middle, so the field feels natural rather than jumpy. Full caret-perfect masking is genuinely hard (it's why libraries exist), but this handles the everyday typing and pasting cases that cover the vast majority of real use.
Mobile keyboards and accessibility
Each field sets the right inputmode (numeric for phone/card/date, decimal for currency) so mobile devices show the appropriate keypad, and type="tel" on the phone field for the same reason. The display value is always the masked, human-readable form; when you submit, strip it back to the raw value (the FAQs show how) so your backend stores clean data. tabular-nums keeps the digits evenly spaced as they format.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA form renders with phone, card, expiry, date, and currency fields, each formatting as you type.
- 2Type a phone numberEnter digits — parentheses and a dash appear automatically as "(555) 123-4567" without typing any punctuation.
- 3Try the card fieldType 16 digits and they group into four space-separated blocks; expiry inserts the slash after two digits.
- 4Test currencyType numbers and a $ prefix, thousands commas, and two-decimal cap appear automatically.
- 5Add your own maskAdd a function to the MASKS object (raw value → formatted string) and a data-mask attribute on the field.
- 6Strip on submitBefore sending, remove the formatting (value.replace(/\D/g, '')) so your backend stores the clean raw value.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Strip the formatting before sending: for digit-only masks, value.replace(/\D/g, '') gives the clean digits (e.g. "5551234567"); for currency, value.replace(/[^\d.]/g, '') gives the number. Always store and validate the raw value server-side — the mask is a display concern, so never persist the formatted string with its punctuation.
Add a function to the MASKS object keyed by a name — it receives the raw input value and returns the formatted string (strip to the allowed characters, cap the length, and insert the separators). Then put data-mask="ssn" on the field. The setup loop wires it automatically; no other code changes.
Inserting punctuation per keystroke breaks on paste, mid-string deletion, and selection-replace, because the intermediate value is messy. Stripping to the raw characters and reformatting from scratch produces a correct result for every editing action, since the output is always derived from clean data. The only tricky part is caret restoration, which this handles for the common cases.
It preserves the natural forward-typing case (caret at end) perfectly and makes a best-effort restore when editing mid-string. Truly caret-perfect masking across every edit and locale is hard — it's the main reason dedicated masking libraries exist. For most forms (where users type left-to-right and occasionally paste) this is more than sufficient; for complex cases, layer a library on top of the same mask functions.
In React, make the input controlled — hold the value in useState and set it to mask(e.target.value) in onChange; in Vue, use a watcher or computed setter on the v-model; in Angular, apply the mask in the (input) handler or a custom directive. The mask functions are pure and port unchanged — they take a raw string and return a formatted one regardless of framework.