Маски ввода и UX-формы
1) Принципы
1. Помогать, не наказывать. Маска направляет ввод и снижает ошибки, но не блокирует печать и вставку.
2. Данные ≠ отображение. Храним «сырые» нормализованные значения, форматируем только в UI.
3. Предсказуемый курсор. Любая автоподстановка не «прыгает» caret и не ломает undo/redo.
4. Локаль и устройство. Клавиатуры, разделители, календарь и валюты — по региону и платформе.
5. Доступность и приватность. Текст + иконка/цвет; чувствительные поля маскируем, но не мешаем менеджерам паролей/автозаполнению.
2) Когда маска уместна (и когда нет)
Использовать:- Форматы с устойчивой структурой: телефон, IBAN, PAN (карты), CVC, дата, время, индекс, OTP.
- Денежные суммы с разделителями (при печати — «чистый» ввод, при блюре — формат).
- Коды (реф.коды, промо), фиксированные длины.
- Имена/адреса/Email (маска ограничивает допустимые символы/языки).
- Сложные свободные поля (комментарии, названия компаний).
- Ввод с потенциально неизвестным форматом (международные номера без страны).
3) Маска vs автоформат vs валидация
Маска — подсказка структуры «на лету» (скобки, дефисы); не должна ломать ввод/вставку.
Автоформат — применяем при блюре/потере фокуса (тысячи, пробелы IBAN).
Валидация — логика корректности (длина, контрольные суммы), показ ошибок после `blur` или `submit`.
Правило: маска не заменяет валидацию, а автоформат не должен менять смысл введенного.
4) Клавиатуры и атрибуты HTML
Подберите корректные типы/режимы, чтобы ускорить ввод и снизить ошибки:5) Карет, копипаст и нормализация
Не ломать caret: при авто-вставке символов (пробелы/скобки) корректируйте позицию курсора.
Копипаст: при вставке очищайте от пробелов/дефисов → валидируйте → отображайте с форматированием.
Нормализация: тримминг, замена «кривых» символов (`O`→`0` нельзя!), перевод в верхний регистр для IBAN, единый формат даты в хранилище (ISO).
js const clean = s => s. replace(/[^\da-zA-Z]/g,'');
const normalizePAN = s => clean(s). slice (0.19) ;//no spaces/hyphens const normalizeIBAN = s => clean (s). toUpperCase(); // A–Z0–9
6) Числа, валюты и локали
Ввод «как печатается» (допуск `,` или `.` как разделителя), хранение в minor units (копейки/центы).
Отображение по локали (группировка тысяч) на блюре/после сабмита; в фокусе показывайте «сырое» значение для удобства редактирования.
Явно указывайте валюту и фиксируйте точность (например, 2 знака).
js function parseMoney(input) {
//resolve both comma and period as decimal const s = input. replace(/\s/g,''). replace(',', '.');
const num = Number(s);
if (Number. isNaN(num)) return null;
return Math. round(num 100); // cents
}
function formatMoney(cents, locale='ru-RU', currency='RUB') {
return (cents/100). toLocaleString(locale, { style:'currency', currency });
}
7) Даты и время
Если нативные пикеры неудобны/разные на платформах — используйте текстовое поле с маской `DD.MM.YYYY`, но храните ISO `YYYY-MM-DD`.
Проверка реальности дат (29.02, диапазоны), таймзоны — на сервере.
Добавьте кнопки «Сегодня», «Сейчас», «Очистить».
8) Телефоны и страны
Два поля: страна (+код) и номер или «умная» маска по выбранной стране.
При вставке полного `+CC …` автозаполните страну.
Храните E.164 (`+CCXXXXXXXXX`), показывайте локально с пробелами.
9) Платежные реквизиты: PAN/IBAN/CVC/EXP
PAN: группировка 4-4-4-4/4; в значении — только цифры; Luhn-check; никаких логов/аналитики с PAN.
CVC: `password`-стиль (скрыто), `autocomplete="cc-csc"`, не сохранять в черновики.
EXP: `MM/YY`, авто-вставка `/` после 2 цифр, проверка диапазона 01–12 и разумного года.
IBAN: upper-case, пробелы только в UI; проверка длины по стране и контрольной суммы.
10) OTP/код подтверждения
6 (или N) ячеек с автофокусом и авто-переходом, вставка из буфера распознает весь код.
`autocomplete="one-time-code"`, на мобильных — автоизвлечение из СМС.
Бэкап-ввод без сплит-полей (одно поле) — для скринридеров.
html
<div class="otp" role="group" aria-label="Код из SMS">
<input inputmode="numeric" maxlength="1">
<input inputmode="numeric" maxlength="1">
<!-- … -->
</div>
11) Маски и a11y
Лейбл обязателен (`<label for>`), placeholder — пример, а не замена.
Объясняйте правило рядом: helper text с примером («Формат: +CC ХХХ ХХХ-ХХ-ХХ»).
Ошибки связывайте через `aria-describedby`, критичные — `role="alert"`.
Контраст текста и контуров ≥ AA, `:focus-visible` не скрывать.
12) Приватность и безопасность
Чувствительные поля: не логировать, не писать в RUM, не сохранять в черновики (PAN, CVC, паспорт).
Маски и форматирование не должны раскрывать валидность учетной записи («Если email зарегистрирован…» — нейтральная формулировка).
Идемпотентность и retry для критичных сабмитов (платеж/ставка).
13) Поведение форм и производительность
Дебаунс асинхронных проверок (250–400 мс), видимая индикация «Проверяем…».
Не блокируйте весь экран ради одного поля; локальный спиннер/скелет.
Батчите изменения DOM; анимируйте только `transform/opacity`.
На мобильных — избегайте «скачков» при появлении клавиатуры (safe-area, viewport meta).
14) Код-сниппеты
Мягкая маска телефона (без ломки вставки):js function formatPhoneVisible(value) {
const d = value. replace(/\D/g,''). slice(0,15);
if (!d) return '';
if (d. startsWith('7') d. startsWith('8')) {
return d. replace(/^([78])? (\d{3})(\d{3})(\d{2})(\d{2})./, '+7 ($2) $3-$4-$5');
}
// generic E.164 grouping: +CC XXX XXX XX XX return d. replace(/^(\d{1,3})(\d{0,3})(\d{0,3})(\d{0,2})(\d{0,2})./, (m,c1,c2,c3,c4,c5)=>
`+${c1}${c2?` ${c2}`:''}${c3?` ${c3}`:''}${c4?` ${c4}`:''}${c5?` ${c5}`:''}`.trim());
}
const input = document. querySelector('#phone');
input. addEventListener('input', e => {
const raw = e. target. value;
const pos = e. target. selectionStart;
const digitsBefore = raw. slice(0,pos). replace(/\D/g,''). length;
const cleaned = raw. replace(/[^\d+]/g,'');
const visible = formatPhoneVisible(cleaned);
e. target. value = visible;
// restore caret by counting digits let p = 0, count = 0;
while (p < e. target. value. length && count < digitsBefore) { if (/\d/.test(e. target. value[p])) count++; p++; }
e. target. setSelectionRange(p, p);
});
Сумма: «сырое в фокусе → формат при блюре»:
js const amount = document. getElementById('amount');
let cents = null;
amount. addEventListener('focus', () => {
if (cents!=null) amount. value = String(cents/100). replace('.', ',');
});
amount. addEventListener('blur', () => {
const v = parseMoney(amount. value) ;//from section 6 if (v = = null) return; cents = v;
amount. value = formatMoney(cents, 'ru-RU', 'RUB');
});
IBAN: upper-case и группировка при блюре:
js const iban = document. getElementById('iban');
iban. addEventListener('input', () => iban. value = iban. value. toUpperCase());
iban. addEventListener('blur', () => {
const raw = normalizeIBAN(iban. value);
iban. dataset. raw = raw ;//for iban submission. value = raw. replace(/(.{4})/g,'$1 '). trim () ;//view only
});
15) Токены дизайн-системы (пример)
json
{
"input": {
"radius": 10,
"height": { "sm": 36, "md": 40, "lg": 48 },
"gap": 8,
"icon": 16
},
"mask": {
"debounceMs": 300,
"otpLength": 6,
"moneyPrecision": 2,
"phoneMaxDigits": 15
},
"a11y": {
"focusRing": { "width": 2, "offset": 2 },
"contrastAA": true
}
}
CSS-пресеты:
css
.input { height:40px; padding:0 12px; border-radius:10px; }
.input:focus-visible { outline:2px solid var(--focus-ring); outline-offset:2px; }
.field-error { color: var(--role-danger); font-size:.875rem; margin-top:6px; }
.otp input { width:40px; text-align:center; }
16) Специфика iGaming
Платежи/выводы: PAN/IBAN/amount с мягкими масками; строгая идемпотентность и отсутствие логов чувствительных полей; подсказки про комиссии и сроки.
KYC: маски для дат, паспортных номеров (без «жесткой» фильтрации — учет разных форматов), размер/тип файла, предпросмотр.
Лимиты и ответственная игра: понятные суммы/периоды (дни/недели/месяцы), хелперы рядом, контраст AAA.
Ставки: быстрый ввод суммы (кнопки-пресеты + поле), в фокусе «сырое» число, при блюре формат по локали; недопуск «,…..»/двойного разделителя.
17) Анти-паттерны
Жесткие маски, запрещающие допустимые символы/вставку.
Прыгающий caret при автоформате; пропажа выделения/undo.
Placeholder вместо лейбла.
Авто-добавление валюты внутрь значения (ломает копипаст).
Ошибки «на каждый символ» без дебаунса.
Локалезависимые форматы в хранилище (храните ISO/числа).
Логирование PAN/паспортных номеров и показ «слишком честных» причин отклонения.
18) Метрики и эксперименты
Error rate по полям (до/после маски).
Time-to-Complete формы и повторные отправки.
Доля неудачных вставок (копипаст) и «откатов» (undo).
CTR подсказок/примеров, доля автозаполнений.
Abandon rate на шаге платежа/KYC.
19) QA-чек-лист
Ввод и caret
- Вставка из буфера не ломается, пробелы/дефисы очищаются корректно.
- Caret остается предсказуемым после автоформата.
Локаль и формат
- Суммы допускают `,`/`.`; хранение в minor units.
- Даты парсятся и валидируются; хранение в ISO.
A11y
- Лейблы и `aria-describedby` подключены; `role="alert"` для критических.
- Контраст и фокус-кольца соответствуют AA.
Безопасность
- Чувствительные поля не логируются/не кэшируются.
- Идемпотентность и retry на критичных шагах.
UX
- Placeholder — пример, не лейбл; helper рядом.
- Маски не препятствуют печати на мобильных; корректные клавиатуры (`inputmode`).
20) Документация в дизайн-системе
Компоненты: `MaskedInput`, `MoneyInput`, `PhoneInput`, `OtpInput`, `IbanInput`.
Токены масок (длины/шаблоны), правила caret/вставки, локализация чисел/дат.
Гайды по приватности (чего не логировать), по доступности и по автоформату vs блюр.
«Do/Don’t» с реальными примерами и метриками до/после.
Краткое резюме
Маски и формы хороши, когда они ускоряют ввод, сохраняют данные чистыми и не мешают. Форматируйте бережно, нормализуйте на входе, храните в стабильных видах, учитывайте локали и доступность. Тогда формы становятся быстрыми и понятными — особенно в чувствительных сценариях платежей, KYC и ставок.