javascript 44 lines · 8 steps

Live thousand separators without losing the caret

An input handler reformats numbers with locale separators while keeping the cursor anchored to the same digit.

Explained by highlit
1function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
2 const formatter = new Intl.NumberFormat(locale);
3 const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
4 const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';
5 
6 const countDigitsBeforeCaret = (value, caret) =>
7 value.slice(0, caret).replace(/[^\d]/g, '').length;
8 
9 const caretForDigitCount = (value, digitsWanted) => {
10 if (digitsWanted === 0) return 0;
11 let seen = 0;
12 for (let i = 0; i < value.length; i++) {
13 if (/\d/.test(value[i])) {
14 seen++;
15 if (seen === digitsWanted) return i + 1;
16 }
17 }
18 return value.length;
19 };
20 
21 const format = () => {
22 const raw = input.value;
23 const digitsBeforeCaret = countDigitsBeforeCaret(raw, input.selectionStart);
24 
25 let [intPart, ...rest] = raw.split(decimalSep);
26 const negative = /^-/.test(intPart);
27 intPart = intPart.replace(/[^\d]/g, '');
28 
29 let formatted = intPart
30 ? formatter.format(BigInt(intPart))
31 : '';
32 if (negative && formatted) formatted = '-' + formatted;
33 if (rest.length) formatted += decimalSep + rest.join('').replace(/[^\d]/g, '');
34 
35 input.value = formatted;
36 const caret = caretForDigitCount(formatted, digitsBeforeCaret);
37 input.setSelectionRange(caret, caret);
38 };
39 
40 input.addEventListener('input', format);
41 format();
42 
43 return () => input.removeEventListener('input', format);
44}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Counting digits before the caret is a locale-agnostic anchor that survives reformatting when raw character positions don't.
  2. 2Intl.NumberFormat can be probed with sample values to discover the actual group and decimal separators for any locale.
  3. 3Returning a cleanup function makes an attach-style helper self-contained and easy to tear down.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Live thousand separators without losing the caret — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code