LEARNING OBJECTIVES ⌵
- Understand the mechanics of real-time input formatting using the DOM
inputandbeforeinputevents. - Diagnose and solve the classic "Cursor Jump to End" bug using
selectionStartandsetSelectionRange(). - Implement robust masking patterns for credit card numbers (with Amex/Visa detection), phone numbers, and dates.
- Support backspace and delete keystrokes across formatting delimiters without getting stuck.
- Configure mobile-friendly input hints using
inputmode,pattern, andautocomplete.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine writing on a paper form with pre-printed boxed grids—four boxes, a dash, four boxes, a dash. If a helpful assistant stands beside you and physically shifts the entire sheet of paper to the left every time you write a single digit, your pen tip will suddenly land in the wrong box. You try to fix a typo in the middle of the number, but every time you make a stroke, the assistant pulls the paper and pushes your pen all the way to the bottom right corner of the page.
This is the infamous Cursor Jumping Bug in JavaScript. When an engineer naively rewrites input.value = format(input.value) inside an input event listener, the browser rendering engine loses track of where the user was typing and throws the caret to the end of the text.
To build a professional input mask, your code must act like a master typist: calculate how many raw digits existed before the pen tip, transform the paper layout, and immediately place the pen tip back down at the exact corresponding character index.
Technical Deep Dive & Specifications
The Anatomy of the Cursor Jumping Problem
When a user edits an input in the middle of a string:
- User types digit
'5'at index7in"4111 11|11 1111". - The
inputevent triggers. - JavaScript reformats the string to
"4111 1151 1111 1". - JavaScript assigns
input.value = newString. - Browser Default Behavior: Assigning to
.valueresetsselectionStartandselectionEndtonewString.length(the very end). - Result: The user is typing in the middle, but subsequent keystrokes appear at the end!
BEFORE REFORMAT:
Value: "4 1 1 1 1 1 [5] 1 1 1 1 1"
Cursor: ^ (Index 7: 5 raw digits prior)
NAIVE REFORMAT:
Value: "4 1 1 1 1 1 5 1 1 1 1 1"
Cursor: ^ (Index 15 - JUMPED TO END!)
ALGORITHMIC CURSOR RESTORATION:
1. Count unmasked characters before old cursor = 5 digits ('4','1','1','1','1').
2. Format new raw string -> "4111 1151 1111 1".
3. Walk formatted string until 5 raw digits are encountered.
4. Target cursor position = index 8.
5. input.setSelectionRange(8, 8).
The Cursor Preservation Algorithm
function formatWithCursorPreservation(input, formatterFn) {
const previousValue = input.value;
const previousCursor = input.selectionStart;
// 1. Count raw valid digits before the cursor prior to formatting
const digitsBeforeCursor = previousValue
.slice(0, previousCursor)
.replace(/\D/g, '').length;
// 2. Compute formatted value from raw characters
const rawDigits = previousValue.replace(/\D/g, '');
const formattedValue = formatterFn(rawDigits);
// 3. Update DOM value
input.value = formattedValue;
// 4. Find new cursor position matching the raw digit count
let newCursor = 0;
let digitCount = 0;
for (let i = 0; i < formattedValue.length; i++) {
if (/\d/.test(formattedValue[i])) {
digitCount++;
}
if (digitCount === digitsBeforeCursor) {
newCursor = i + 1;
break;
}
}
// Edge case: if no digits before cursor, place at start
if (digitsBeforeCursor === 0) newCursor = 0;
// 5. Restore cursor position
input.setSelectionRange(newCursor, newCursor);
}
Common Mask Formatting Rules
+-------------------------------------------------------------------------------+
| Pattern Name | Format Template | Regex Token Transformation |
+-------------------------------------------------------------------------------+
| Standard Credit | #### #### #### #### | (\d{4})(?=\d) -> '$1 ' |
| Amex Card | #### ###### ##### | (\d{4})(\d{6})? -> '$1 $2 $3' |
| US Phone Number | (###) ###-#### | (\d{3})(\d{3})(\d{4}) |
| Expiration Date | MM/YY | (\d{2})(?=\d) -> '$1/' |
+-------------------------------------------------------------------------------+
Mobile Input Optimizations
Masking must be paired with correct HTML5 mobile hints:
inputmode="numeric": Pops up the numeric dial pad on iOS/Android without showing the full alpha keyboard.autocomplete="cc-number": Enables 1-tap browser autofill and camera card scanning.autocomplete="tel": Enables phone number autofill from contacts.
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 101–105 (
const digitsBeforeCursor = ...): Analyzes the substring from index0up toselectionStart, counting purely numeric digits while ignoring space or punctuation delimiters. - Line 108 (
const raw = prevVal.replace(/\D/g, '')): Cleans the input stream of all non-numeric characters before passing to the pattern formatter. - Line 111 (
inputElement.value = formatted): Updates the DOM input string with appropriate spacing, dashes, or parentheses. - Lines 114–121 (
for (let i = 0; i < formatted.length; i++)): Iterates through the freshly formatted string to map where the Nth digit now resides, pinpointing the exact character index for the caret. - Line 124 (
inputElement.setSelectionRange(newCursor, newCursor)): Programmatically pins the cursor at the calculated position, eliminating cursor jumping bugs. - Lines 134–146 (
ccBadge detection): Checks the Major Industry Identifier (MII) prefixes (4 for Visa, 51–55 for Mastercard, 34/37 for American Express) and switches formatting from 4-4-4-4 to 4-6-5 on the fly.
Expected Browser Render Output
(Typing in the middle of any field immediately retains the caret directly adjacent to the edited digit rather than snapping to the end.)
+-----------------------------------------------------------+
| Payment & Contact Details |
| |
| Credit Card Number |
| [ 4111 2222 3333 4444 ] [ VISA ] |
| |
| Expiry Date Phone Number |
| [ 12/28 ] [ (415) 555-0199 ] |
| |
| Raw State: CC Raw: "4111222233334444" | Exp: "12/28" ... |
+-----------------------------------------------------------+🏋️ Hands-On Exercise
🎯 The Challenge: Currency Mask with Delimiter & Precision Control
Instructions:
- Build a real-time currency formatter input (
$ 1,234,567.89). - Format rules:
- Always prefix with
$. - Insert comma
,grouping for every 3 integer digits. - Allow a maximum of 1 decimal dot
.and at most 2 fractional decimal digits. - Prevent entering letters or multiple decimal points.
- Always prefix with
- Maintain stable cursor positioning when users edit numbers in the thousands or millions columns.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Filtering Keydown Codes Instead of Processing
input: Interceptingkeydownand callinge.preventDefault()on non-digits breaks pasting via Ctrl+V/Cmd+V, voice dictation, and password manager autofill. Always listen to theinputevent and sanitize the updated string. - Neglecting the
selectionStartRestoration: Modifyinginput.valuewithout restoringsetSelectionRange()causes intolerable jumping bugs when users make edits in the middle of long numbers. - Storing Formatted Delimiters in the Backend: Submitting
(555) 019-2834to your API creates messy database records. Always strip delimiters before serialization (rawDigits = value.replace(/\D/g, '')) or store in an unmasked hidden field.
💡 Pro Tips
- Leverage
inputmodefor Instant Mobile Keyboards: Specifyinginputmode="numeric"orinputmode="decimal"on text inputs opens the native numeric keypad on iOS and Android without triggering browser validation constraints that<input type="number">enforces. - Handle Backspace on Delimiters: When a user presses backspace directly after a space or hyphen, detect
e.inputType === 'deleteContentBackward'and delete the preceding digit rather than just the delimiter, preventing the mask from getting "stuck".
📌 Key Takeaways
- Modifying
input.valueprogrammatically resets the DOM caret to the end of the input string. - Preserve cursor position by counting valid raw characters prior to
selectionStartand restoring viasetSelectionRange(). - Listen to the
inputevent to accommodate typing, pasting, autofill, and voice dictation. - Always configure
inputmode="numeric"and standardautocompletetokens for frictionless mobile UX. - Sanitize masked values back to raw numbers or ISO standards before sending payloads to backend endpoints.
- --