Доступность на этапе дизайна: контраст, размеры, состояния, порядок
Типичная сцена. Команда решила «заняться доступностью». Разработчик проходится по компонентам, расставляет aria-label, чинит роли, добавляет role="dialog" и ловушку фокуса в модалке. Автотест на axe становится зелёным. А потом приходит пользователь, у которого возрастная дальнозоркость и телефон на солнце, и он не видит подпись под полем, не находит кнопку «Оплатить» и трижды промахивается мимо иконки удаления.
Ничего удивительного: барьеры, которые он встретил, были заложены не в коде — они были нарисованы. Серый #9aa0a6 на белом фоне, иконка 16 × 16 без отступов, отсутствие состояния фокуса в макете (его просто никто не отрисовал), кнопка-призрак с границей контрастом 1.6:1 — это решения дизайна. Разработчик может либо повторить их «пиксель в пиксель», либо начать спор.
Эта статья — про ту часть доступности, которая решается до кода. Полная картина — стандарты, ARIA, скринридеры, тестирование — разобрана в отдельном треке: начните с обзора трека «Доступность». Здесь — только то, что дизайнер обязан решить в макете, и то, как это передать в разработку, чтобы решение выжило.
Что решается в макете, а что в коде
Полезно провести границу явно — иначе начинается перекладывание ответственности.
| Решение | Кто закладывает | Где ломается |
|---|---|---|
| Контраст текста и границ | дизайн | палитра, токены |
| Размер и отступы зон нажатия | дизайн | компонент, плотные таблицы |
| Состояния: hover, focus, disabled, loading, error | дизайн | «не нарисовали — не сделали» |
| Иерархия заголовков | дизайн | стили текста без имён уровней |
| Порядок чтения и фокуса | дизайн (структура) → код (разметка) | двухколоночные раскладки |
| Текст лейблов, ошибок, кнопок | дизайн + редактор | плейсхолдеры вместо лейблов |
| Роли, семантика, ARIA | код | div вместо button |
| Управление фокусом при переходах | код (по сценарию дизайна) | модалки, роутинг |
или моторику?"} B -->|да| C["Контраст, размер,
состояние, порядок"] B -->|нет| D["Стиль: скругления,
тени, плотность"] C --> E{"Есть измеримый порог?"} E -->|есть| F["Проверяем числом
до передачи в код"] E -->|нет| G["Проверяем на людях —
юзабилити-тест"] D --> H["Спорим о вкусе,
решает владелец стиля"] F --> I["Аннотация в макете"] G --> I I --> J["Разработка"] J --> K{"Проверка сборки
клавиатурой и зумом"} K -->|не совпало| C K -->|ок| L["Релиз"]
Главная мысль: у части дизайнерских решений есть числовой критерий, проверяемый за минуту в макете. Спорить о них после релиза — дорого и бессмысленно.
Для кого мы это делаем
Разговор о доступности часто буксует, потому что в голове возникает образ «слепого пользователя» и оценка «их же мало». Полезнее модель Microsoft Inclusive Design: постоянное, временное, ситуативное ограничение — человек без руки, человек с рукой в гипсе и человек с ребёнком на руках решают одну и ту же задачу одной рукой. Проектируем под ограничение, а не под диагноз. Несколько цифр, которые меняют ощущение масштаба:
- Нарушения зрения разной степени — у более чем 2,2 млрд человек по оценке ВОЗ. Подавляющее большинство из них — не слепота, а «плохо вижу мелкое и бледное».
- Нарушение цветовосприятия — примерно у 8% мужчин европейского происхождения и 0,5% женщин (Colour Blind Awareness).
- Пресбиопия — то есть ухудшение фокусировки вблизи — начинается в среднем после 40 лет и дальше есть практически у всех. Это не меньшинство, это ваши платящие пользователи.
- По ежегодному аудиту WebAIM Million недостаточный контраст текста — самая частая обнаруживаемая ошибка: она встречается примерно на четырёх из пяти главных страниц крупных сайтов. Это ошибка дизайна, а не разработки.
Отсюда практический вывод: доступность на этапе дизайна — это не «спецрежим для инвалидов», это базовое качество восприятия интерфейса в неидеальных условиях. Солнце, дешёвая матрица, усталость в конце дня, метро и тряска — те же ограничения, только временные.
Контраст: считаем, а не подбираем на глаз
Как устроен коэффициент контрастности
WCAG 2.x определяет контраст через относительную яркость. Сначала каждый канал sRGB линеаризуется (снимается гамма), затем берётся взвешенная сумма:
$$L = 0.2126 R_{lin} + 0.7152 G_{lin} + 0.0722 B_{lin}$$
Коэффициенты неравные, потому что глаз чувствительнее к зелёному и почти не чувствителен к синему. Отсюда практическое следствие, которое стоит выучить: синий тёмный почти всегда, жёлтый светлый почти всегда. Чисто синий #0000FF на чёрном даёт около 2.4:1 — то есть синий на тёмной теме читается плохо, как ни двигай насыщенность.
Сам коэффициент:
$$C = \frac{L_{light} + 0.05}{L_{dark} + 0.05}$$
Слагаемое 0.05 моделирует паразитную засветку экрана: идеального чёрного не бывает. Из-за него диапазон значений — от 1:1 до 21:1.
Пороги, которые нужно помнить
| Что | Порог AA | Порог AAA | Критерий WCAG 2.2 |
|---|---|---|---|
| Обычный текст | 4.5:1 | 7:1 | 1.4.3 |
| Крупный текст: от 24 CSS px, либо от 18.66 px полужирным | 3:1 | 4.5:1 | 1.4.3 |
| Границы контролов, иконки, индикаторы состояния | 3:1 | — | 1.4.11 |
| Индикатор фокуса относительно соседнего цвета | 3:1 | — | 1.4.11 и 2.4.13 |
| Отключённые контролы, логотипы, декор | не нормируется | — | исключения 1.4.3 |
Разбор: почему «фирменный жёлтый» не работает
Классический конфликт. У бренда акцентный жёлтый #FFC400. Дизайнер делает основную кнопку жёлтой с белым текстом — «как в брендбуке». Считаем: белый на #FFC400 даёт примерно 1.7:1. Текст исчезает при любом внешнем свете.
Правильный разбор — не «жёлтый плохой», а «жёлтый светлый», а значит:
- Жёлтый — это фон под тёмным текстом, а не под светлым.
#1A1A1Aна#FFC400даёт около 12:1. - Если бренд требует жёлтые буквы, они живут на тёмном фоне, а не на белом.
- Для ссылок и мелкого текста жёлтый не подходит вовсе: нужен затемнённый вариант того же тона.
Это типовой ответ на «нам нельзя менять фирменный цвет»: цвет не меняется, меняется его роль. Роли — это уже работа с токенами, см. https://courses.digitable.life/post/ux-design/07-design-systems/.
Разбор: серый плейсхолдер
Второй по частоте случай: подсказка в поле цветом #999999 на белом. Это 2.85:1 — ниже порога. Дизайнер выбрал такой серый специально: «подсказка не должна конкурировать с введённым текстом». Задача понятная, решение неверное. Что чинит проблему, не ломая задачу:
- Убрать плейсхолдер как носитель смысла: подпись поля выносится наружу, плейсхолдер либо исчезает, либо содержит только формат («+7 900 000-00-00»).
- Различать текст и подсказку не яркостью, а весом и размером: подпись 13/16 обычным весом, введённое значение 15/20. Иерархия сохраняется, контраст остаётся выше 4.5:1.
- Если серый всё-таки нужен, взять
#6E6E6E— это уже 5.3:1 на белом и визуально всё ещё «тише» чёрного.
Практика: палитра парами, а не цветами
Дизайнер выбирает цвета, но пользователь видит пары. Поэтому палитра описывается не списком «primary-500, gray-400», а таблицей допустимых сочетаний. В токенах это выглядит как пары «поверхность → текст на ней».
"""Проверка контраста по WCAG 2.x: O(1) на пару, O(n*m) на матрицу палитры."""
def _linear(c: float) -> float:
"""Снимаем гамму sRGB; c — доля канала от 0 до 1."""
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def luminance(hex_color: str) -> float:
"""Относительная яркость: веса отражают чувствительность глаза."""
v = hex_color.lstrip("#")
r, g, b = (int(v[i:i + 2], 16) / 255 for i in (0, 2, 4))
return 0.2126 * _linear(r) + 0.7152 * _linear(g) + 0.0722 * _linear(b)
def ratio(fg: str, bg: str) -> float:
"""Коэффициент контрастности от 1.0 до 21.0."""
a, b = luminance(fg), luminance(bg)
return (max(a, b) + 0.05) / (min(a, b) + 0.05)
def check(fg: str, bg: str, *, small_text: bool = True) -> str:
"""Вердикт для ревью макета: 4.5 для обычного текста, 3.0 для остального."""
limit = 4.5 if small_text else 3.0
value = ratio(fg, bg)
return f"{fg} на {bg}: {value:.2f}:1 (порог {limit}) — " + ("ок" if value >= limit else "НИЖЕ")
if __name__ == "__main__":
print(check("#FFFFFF", "#FFC400")) # белый на фирменном жёлтом: 1.70
print(check("#1A1A1A", "#FFC400")) # тёмный на нём же: около 12
print(check("#999999", "#FFFFFF")) # серый плейсхолдер: 2.85
print(check("#6E6E6E", "#FFFFFF")) # затемнённый серый: 5.32
print(check("#D0D0D0", "#FFFFFF", small_text=False)) # граница поля: 1.47
Такой скрипт стоит гонять по токенам в CI — он ловит регрессии палитры раньше, чем дизайн-ревью. Как встроить проверку в конвейер, разобрано в тестировании и процессе доступности.
Честно про пороги: где заканчивается наука
Формула WCAG 2.x — это компромисс двадцатилетней давности, и у неё известные артефакты:
- Она плохо описывает тёмную тему. Пара «светлый текст на тёмном фоне» с формально тем же коэффициентом воспринимается иначе из-за эффекта разлития (halation) — светлые буквы на тёмном «толстеют». На практике для тёмной темы часто берут не чистый белый (
#FFFFFF), а#E6E6E6, и не чистый чёрный фон, а#14161A. - Она игнорирует толщину шрифта. Тонкий Light 14 px и полужирный 14 px с одинаковым контрастом читаются по-разному, а формула этого не видит.
- На замену готовится APCA — модель, которая учитывает полярность и вес шрифта; она обсуждается как основа WCAG 3. Пока это черновик: считать по ней полезно для проверки себя, ссылаться в требованиях — рано.
Вывод для практики: WCAG 2.x — обязательный минимум и юридический ориентир, но проходить его «впритык» на 4.51:1 — плохая идея. Держите запас, особенно для тонких шрифтов и тёмной темы.
Размеры и попадание: почему промахиваются
Закон Фиттса без мистики
Время наведения на цель растёт с расстоянием и падает с размером цели:
$$MT = a + b \log_2 \left(\frac{2D}{W}\right)$$
где $D$ — дистанция до цели, $W$ — её размер вдоль оси движения. Это одна из самых воспроизводимых закономерностей в HCI (обзор — у NN/g). Из неё следуют не «правила», а понятные проектные ходы:
- Увеличение мелкой цели даёт больше выигрыша, чем сокращение расстояния: логарифм от отношения.
- Края и углы экрана — бесконечно «большие» цели, потому что курсор в них упирается. Отсюда работают прилипшие к краю панели.
- На тач-устройстве «расстояние» — это ещё и перехват большого пальца: цель внизу экрана дешевле.
Важно не заигрываться: карты «зон большого пальца» — эвристика, а не закон. Люди держат телефон десятком способов, а половина использует две руки. Проверять на людях, см. https://courses.digitable.life/post/ux-design/11-usability-testing/.
Пороги размеров
- 24 × 24 CSS px — минимум AA в WCAG 2.2, критерий 2.5.8 Target Size (Minimum). Есть исключение через отступ: если сама цель меньше, вокруг её центра должен помещаться круг диаметром 24 px, не пересекающийся с кругом соседней цели.
- 44 × 44 pt — рекомендация Apple в HIG и порог AAA (2.5.5).
- 48 × 48 dp — рекомендация Material Design с минимальным зазором 8 dp.
Ключевое, что видно на схеме: зона нажатия не обязана совпадать с видимым глифом. Иконка может остаться 16 × 16, если вокруг неё есть невидимая область нажатия. Это снимает главный аргумент дизайнеров против крупных целей — «интерфейс станет рыхлым».
Разбор: промах в плотной таблице
Ситуация: административная таблица, в каждой строке иконки «редактировать» и «удалить», 16 px, расстояние между ними 4 px. Жалобы: «удалил не ту запись». Разбираем по слоям:
- Моторика. Пятно контакта пальца — около 10 мм, это примерно 38 CSS px. Две цели по 16 px с зазором 4 px помещаются внутрь одного касания целиком. Промах не вероятность, а норма.
- Цена ошибки. Соседство «безопасного» и «разрушительного» действия — отдельная ошибка дизайна. Действия с разной ценой не ставят рядом одинаковыми по весу.
- Обратимость. Если удаление необратимо, отсутствие отмены — третья ошибка.
Как чинится в макете:
- Зона нажатия каждой иконки — 32 × 32 минимум (в плотных таблицах это компромисс между 24 и 44), зазор между зонами 8 px.
- «Удалить» уезжает в меню действий либо получает подтверждение; на экране остаётся только частое безопасное действие.
- Появляется тост «Запись удалена. Отменить» с окном 5–10 секунд. Отмена дешевле подтверждения: она не тормозит 99% успешных сценариев.
Обратите внимание: из трёх правок только первая — про доступность в узком смысле. Остальные две просто делают интерфейс лучше для всех. Это общее свойство: доступность редко бывает отдельной работой, чаще это дисциплина принятия обычных решений.
Состояния: то, чего нет в макете, не будет в продукте
Самая дешёвая и самая частая дизайнерская ошибка — отрисовать компонент в одном состоянии. Дальше разработчик выдумывает остальные, а «выдумывает» на практике значит «берёт дефолт браузера или отключает его».
Минимальный набор, который дизайнер отдаёт для любого интерактивного элемента: покой, наведение, фокус, нажатие, отключено, загрузка, ошибка, успех, а для контейнеров — ещё пустое состояние и состояние «данные не загрузились». Подробнее про сами состояния и обратную связь — https://courses.digitable.life/post/ux-design/08-interaction/.
Фокус — не украшение, а навигация
Индикатор фокуса — единственный способ понять, где ты находишься, если ты не пользуешься мышью. Мышью не пользуются не только незрячие: клавиатура быстрее при заполнении форм, у части людей тремор или травма, а на телевизорах и в автомобилях мыши нет вовсе. Что должно быть в макете:
- Стиль кольца фокуса как отдельный токен: цвет, толщина (обычно 2 px), отступ от границы (
offset2 px). Отступ важен: кольцо вплотную к тёмной кнопке сливается с ней. - Контраст кольца к обоим соседним цветам — к фону и к самому контролу — не ниже 3:1. Критерий 2.4.13 Focus Appearance (AAA) формализует ещё и минимальную площадь.
- Смысл не живёт в тултипе. Подсказка по
hoverнедоступна клавиатуре, тачу и скринридеру (критерий 1.4.13 требует навести, задержать и закрыть с клавиатуры). Если объяснение есть только в тултипе — вынесите его на экран, тултип оставьте для дублирования. - Правило «фокус не перекрыт» — критерий 2.4.11 (AA в WCAG 2.2). Прилипшая шапка или плавающая панель косячат именно здесь: элемент получил фокус, но уехал под шапку. В макете это ловится проверкой «а что если сфокусировать элемент под липким хедером».
:root { /* токен фокуса живёт в теме, а не в компоненте */
--focus-ring: 2px solid #2563eb; /* 3.7:1 к белому, 4.1:1 к #f5f5f5 */
--focus-offset: 2px; /* отступ, иначе кольцо сливается с кнопкой */
}
.button:focus-visible { /* при клике мышью кольца не будет */
outline: var(--focus-ring);
outline-offset: var(--focus-offset);
}
.button--inverse:focus-visible { outline-color: #ffffff; } /* на тёмном фоне */
Аргумент «кольцо портит вид» закрывается :focus-visible: при клике мышью кольца нет. Если и после этого хочется убрать — значит, нужно нарисовать другое кольцо, а не выключить. Механика фокуса на стороне кода разобрана в клавиатуре и фокусе.
Разбор: «пользователь не нашёл кнопку»
Реальная жалоба после релиза: конверсия в оплату упала, в записях сессий видно, что люди скроллят вверх-вниз и уходят. Кнопка на экране есть. Разбираем причины по убыванию частоты:
| Симптом | Причина | Правка в макете |
|---|---|---|
| Кнопка выглядит как подпись | «Призрак»: граница контрастом 1.6:1, текст серый | Основное действие — заливка, контраст границы не ниже 3:1 |
| Кнопка ниже сгиба, скролл не очевиден | Блок заканчивается ровно по границе экрана | Обрезать следующий блок наполовину — намёк на продолжение |
| Не понимают, что кнопка сделает | Текст «Продолжить» | «Оплатить 2 400 ₽» — глагол и объект, см. https://courses.digitable.life/post/ux-design/09-microcopy/ |
| Ищут кнопку не там | Позиция ломает выученный паттерн платформы | Держать конвенцию: iOS — справа сверху, Android — внизу |
| Видят, но не могут нажать | Прозрачный слой перекрывает кнопку, зона нажатия меньше визуала | Проверить зону нажатия и порядок слоёв |
Заметьте, что «пользователь не нашёл» почти никогда не про то, что кнопка мала. Чаще про то, что она не читается как кнопка. Это стык доступности и обычного визуального дизайна: https://courses.digitable.life/post/ux-design/06-visual-basics/.
Разбор: отключённая кнопка
Классика: форма с кнопкой disabled, пока не заполнены все поля. Что не так:
- Отключённый элемент по умолчанию не получает фокус — клавиатурный пользователь не может даже дойти до него и понять, что кнопка есть.
- Он не объясняет, чего не хватает. Пользователь смотрит на серую кнопку и гадает.
- Он рисуется бледным, и на исключение из требований контраста ссылаются как на индульгенцию — а человек всё равно должен прочитать его текст.
- Скринридер часто просто пропускает такой элемент при навигации по кнопкам.
Как лучше: кнопка активна всегда. При нажатии с незаполненными полями — валидация, сводка ошибок над формой и перевод фокуса на первое проблемное поле. Пользователь получает ответ на вопрос «почему не работает», а не тупик. Если отключение всё же обязательно (действие недоступно по правам), рядом должно быть объяснение текстом, а не только тултип по наведению.
Порядок: визуальный, логический, фокусный
Есть три порядка, и они должны совпадать:
- Визуальный — как глаз сканирует экран.
- Порядок чтения — в каком порядке контент идёт в разметке (его слышит скринридер).
- Порядок фокуса — как Tab обходит интерактивные элементы.
Расхождение между ними — почти всегда следствие раскладки, придуманной в макете.
Разбор левой части схемы. Дизайнер собрал форму как «две колонки», потому что так красивее по сетке. Разработчик честно сверстал два вертикальных контейнера. Итог: Tab идёт «Имя → Город → Телефон → Фамилия → Индекс → E-mail». Зрячий пользователь мыши этого не заметит. Клавиатурный потеряется, а скринридер прочитает адрес вперемешку с контактами.
Чинится не в CSS. Порядок должен быть заложен структурой: сетка собирается по строкам, а пары полей, которые заполняются вместе, живут в одной строке. Побочный выигрыш: при сужении до мобильной ширины колонки схлопываются, и порядок не меняется.
Правило, которое стоит повесить над столом: макет обязан иметь один осмысленный линейный порядок. Если вы не можете прочитать экран сверху вниз одним потоком и он остаётся понятным — экран спроектирован неправильно, а не «просто нуждается в ARIA».
Заголовки: иерархия текста — это структура документа
Скринридер умеет обходить страницу по заголовкам, и для многих пользователей это основной способ навигации. Уровни заголовков берутся из макета: то, что дизайнер называет «Title / Section / Subsection», в коде становится h1–h3. Что делать дизайнеру:
- Называть стили текста по семантике, а не по размеру.
H2 / 20-28 Semibold— хорошее имя.Text Large Bold— плохое: разработчик не поймёт уровень. - Не пропускать уровни ради размера. Если нужен «маленький h2», это вопрос стиля, а не уровня.
- Помечать регионы: шапка, основной контент, навигация, подвал. Это ориентиры, по которым тоже навигируют. Подробности — в семантическом фундаменте.
Иерархия заголовков — это, по сути, оглавление экрана. Хороший тест: выпишите одни заголовки подряд. Получился внятный конспект — структура в порядке. Получился набор слов — экран структурирован неправильно, и это видно ещё на этапе https://courses.digitable.life/post/ux-design/03-information-architecture/.
Порядок при переходах и в модалках
Дизайнер должен описать словами (или стрелками на макете):
- Куда уходит фокус при открытии модального окна — обычно на заголовок или первый контрол.
- Куда он возвращается при закрытии — на элемент, который её открыл.
- Что делает Esc.
- Куда уходит фокус после отправки формы: на сообщение об успехе или на сводку ошибок.
Это не «детали реализации». Это сценарий, и без него разработчик примет решение сам — скорее всего, никакое.
Не только цветом
Критерий 1.4.1: цвет не может быть единственным носителем информации. Разбор трёх типовых мест:
- Статусы в таблице. Зелёная и красная точки без подписи. Чинится добавлением текста («Оплачен» / «Просрочен») или разной формы значка. Заодно таблица становится копируемой и понятной в чёрно-белой печати.
- График с несколькими линиями. Различие только по цвету легенды. Чинится подписями прямо у линий, разной пунктирностью, разными маркерами точек.
- Обязательные поля. Красная звёздочка без легенды. Чинится либо явным текстом «Необязательно» у необязательных (их обычно меньше), либо легендой в начале формы.
Проверка занимает секунду: посмотрите на макет в градациях серого. Если различие исчезло — информация закодирована только цветом. Симуляторы дальтонизма (Stark, Able, встроенные фильтры macOS) дают более точную картину, но grayscale ловит большинство случаев.
Движение, время и мигание
- Анимация. Крупные параллаксы, зумы и слайды всего экрана вызывают тошноту и головокружение у людей с вестибулярными расстройствами — подробно разобрано у Val Head в A List Apart. В макете это значит: для каждой заметной анимации нужен вариант «сокращённое движение» — обычно простое появление без сдвига. Системный флаг называется
prefers-reduced-motion. - Автоматическое движение. Карусель, которая переключается сама, — препятствие для чтения и для управления. Критерий 2.2.2 требует возможность остановить. Проще не делать автопрокрутку.
- Таймауты. Сессия, которая истекает посреди заполнения длинной формы, — грубый барьер (критерий 2.2.1). Решение в дизайне: предупреждение за минуту, кнопка «продлить», сохранение черновика.
- Мигание. Больше трёх вспышек в секунду — риск фотосенситивного приступа (2.3.1). Это редкий, но опасный случай; проверяйте видеофоны и «праздничные» эффекты.
Текст, масштаб и переверстка
Дизайнер обычно рисует два макета: 1440 и 375. Пользователь живёт между ними и вокруг них.
- Базовый размер. 16 px — не догма, но «дизайнерские» 13–14 px для основного текста означают, что человек с пресбиопией будет зумить. Мелкий шрифт — осознанный расход, а не бесплатная эстетика.
- Масштабирование до 200% (критерий 1.4.4) и переверстка на 320 CSS px по ширине (1.4.10 Reflow). Заметьте: десктоп с зумом 200% — это те же ~720 CSS px. Проверка «а как это выглядит при зуме» ловит фиксированные высоты и обрезанный текст раньше, чем тестировщик.
- Интервалы текста (1.4.12): пользователь может увеличить межстрочный интервал до 1.5, межбуквенный до 0.12em. Если у кнопки фиксированная высота, текст в ней обрежется. Отсюда правило: высота контейнера растёт от содержимого, а не задаётся числом.
- Длина строки. 45–75 символов — рабочий диапазон, за которым глаз теряет строку при возврате.
- Выравнивание по ширине создаёт «реки» пробелов и мешает дислексикам. В вебе без переносов — почти всегда хуже, чем по левому краю.
- CAPS. Слово капсом читается медленнее: пропадает контур слова. Для кнопок и меток — терпимо, для абзацев — нет.
- Локализация. Немецкий текст длиннее русского на 10–30%, а составные слова не переносятся. Если макет собран под точную длину строки, локаль его сломает. Рисуйте худший случай.
Формы: разбор неудобной формы по пунктам
Форма — то место, где недоступность превращается в потерянные деньги. Возьмём типовую форму оформления заказа и разберём, что в ней не так.
| Как обычно сделано | Что при этом ломается | Как чинится в макете |
|---|---|---|
| Плейсхолдер вместо подписи | Подсказка исчезает при вводе; при проверке перед отправкой человек уже не помнит формат | Подпись над полем видна всегда, плейсхолдер — только пример формата |
| Номер карты разбит на четыре поля | Автозаполнение не срабатывает, скринридер читает «поле без имени», вставка из менеджера паролей ломается | Одно поле, форматирование при вводе, inputmode для цифровой клавиатуры |
| Ошибка показана только красной рамкой | Человек с дейтеранопией видит серую рамку и не понимает, где проблема | Иконка плюс текст под полем: что не так и что сделать |
| Сводка ошибок вверху, фокус остался на кнопке | Клавиатурный и незрячий пользователь не знают, что вообще что-то произошло | Фокус переводится на сводку, из неё — ссылки на поля |
| Валидация на каждый символ | Ошибка «неверный e-mail» появляется после первой буквы и обучает игнорировать сообщения | Проверка при потере фокуса, снятие ошибки — сразу при исправлении |
Кнопка disabled, пока форма не заполнена |
Непонятно, чего не хватает; до кнопки нельзя дойти табом | Кнопка активна, ошибки показываются после нажатия |
Дополнительно к таблице — вещи, которые часто забывают:
- Плейсхолдер вместо подписи остаётся самой распространённой ошибкой в формах; NN/g собрал доводы в отдельной статье. Кроме исчезновения подсказки, плейсхолдер обычно ещё и низкоконтрастный — двойной удар.
- Автозаполнение. Поля должны быть распознаваемы (критерий 1.3.5): имя, e-mail, адрес, карта. В макете это значит — не выдумывать нестандартные поля и подписывать их обычными словами. Пользователю с двигательными ограничениями автозаполнение экономит минуты.
- Повторный ввод (3.3.7, WCAG 2.2): не заставляйте вводить то, что уже вводили в этом процессе. Адрес доставки, совпадающий с адресом плательщика, — чекбокс, а не второй блок полей.
- Аутентификация (3.3.8): не требуйте когнитивных тестов — распознавания картинок, ввода кода по памяти без возможности вставки. Запрет вставки в поле кода — маленькая, но реальная жестокость; человек с тремором просто не введёт шесть цифр за 30 секунд.
Разработческая сторона форм — валидация, состояния, ARIA — в формах трека доступности и во фронтенде.
Где наука, а где вкус
Честный разговор, который редко ведут вслух. Часть дизайнерских решений имеет измеримый критерий, часть — опирается на устойчивые закономерности с оговорками, а часть — чистая эстетика. Смешивать эти категории вредно: одинаково плохо и защищать вкусовщину ссылкой на «исследования», и отмахиваться от требований контраста как от «вкусовщины».
Измеримо и обязательно. Контраст, размеры целей, видимость фокуса, дублирование цвета, работа при зуме, порядок фокуса. Здесь нет места мнению: есть порог, есть проверка. Аргумент «мне кажется, читается нормально» — не аргумент, потому что вы смотрите на калиброванный монитор в помещении со стабильным светом.
Закономерность с оговорками. Закон Фиттса — воспроизводим, но параметры $a$ и $b$ зависят от устройства и человека. Закон Хика (время выбора растёт логарифмически с числом альтернатив) верен для однородных списков и плохо переносится на меню с группировкой. F-паттерн сканирования — наблюдение для текстовых страниц, а не универсальный закон композиции. Всё это — источник гипотез, а не готовых решений.
Мифы, которые выдают за науку. «Правило трёх кликов» никогда не подтверждалось: пользователи готовы кликать больше, если каждый шаг очевиден. «Всё важное выше сгиба» — верно как приоритизация и неверно как запрет скроллить. «Serif читается лучше, чем sans-serif» — многократно проверялось и устойчивого эффекта не даёт.
Чистый вкус. Радиусы, тени, плотность, выбор гарнитуры внутри разумного, «воздух». Здесь спор решает владелец стиля — обычно арт-директор или дизайн-система.
Практическое правило спора: разложите аргумент на «пользователь не сможет» и «мне не нравится». Первое требует порога или теста, второе — решения владельца. Смешанный аргумент («крупный фокус — это некрасиво, и вообще никто не пользуется клавиатурой») нужно расщепить и обсуждать по частям. Хорошая опора для формулировок — классические 10 эвристик Нильсена: они не заменяют исследования, но дают общий язык.
Передача в разработку: что такое нормальный хендофф
Доступность умирает на стыке. Дизайнер «всё учёл», разработчик «сделал по макету», в проде кольца фокуса нет, а кнопка стала div. Разберём, из чего состоит нормальная передача.
Что входит в передачу
- Все состояния каждого компонента, а не «главный экран». Отдельно — пустое состояние, состояние ошибки загрузки, состояние с длинным текстом и состояние «одна запись».
- Правила поведения, а не набор ширин. Что сжимается, что переносится, что обрезается многоточием, что скрывается, минимальная и максимальная ширина колонок. Одна страница текста заменяет пять артбордов.
- Токены вместо значений.
color/text/secondary, а не#6E6E6E. Это единственный способ не потерять контраст при смене темы; см. https://courses.digitable.life/post/ux-design/07-design-systems/. - Аннотации доступности — отдельный слой в макете: пронумерованный порядок фокуса; уровень каждого заголовка; alt-тексты для содержательных изображений и пометка «декоративное» для остальных; подписи иконочных кнопок (они станут доступным именем); что и каким текстом объявляется при асинхронных изменениях; что делают Esc, Enter и стрелки в кастомных виджетах.
- Реальные тексты — длинные, пустые, на второй локали — и список открытого: что можно менять при реализации, а что нерушимо.
Формат аннотаций может быть любым — метки в Figma, таблица, YAML рядом с макетом. Важно, что информация существует до реализации, а не появляется в ревью:
# a11y-annotations/checkout-payment.yaml
screen: "Оплата заказа"
landmarks: ["main: Оплата"]
headings: ["h1: Оплата заказа", "h2: Способ оплаты", "h2: Адрес доставки"]
focus_order:
- "Ссылка: назад в корзину"
- "Радиогруппа: способ оплаты" # стрелки переключают, Tab выходит из группы
- "Поля: номер карты, срок, CVC" # номер карты одним полем, не четырьмя
- "Чекбокс: адрес совпадает с плательщиком"
- "Кнопка: Оплатить 2 400 ₽"
announcements:
- trigger: "Ошибка валидации после отправки"
politeness: assertive
text: "Не удалось оплатить. Проверьте 2 поля."
focus_moves_to: "Сводка ошибок"
images:
- id: "card-brands" # декоративное, alt пустой
decorative: true
- id: "delivery-map"
alt: "Карта с отмеченным пунктом выдачи на улице Ленина, 12"
keyboard:
esc: "Закрыть окно смены адреса, фокус возвращается на кнопку «Изменить»"
Почему «пиксель в пиксель» — плохая цель
Требование пиксельного совпадения звучит как забота о качестве, а работает против него.
- Макет — снимок одного состояния при одной ширине. Интерфейс — семейство состояний на континууме ширин, при разных настройках шрифта и разных языках. Совпадение с одним снимком ничего не говорит о остальных.
- Оно толкает к плохой технике. Чтобы совпасть точно, проще нарисовать
divсо стилями, чем взять нативныйselect, у которого свой вид в каждой ОС. Дальше — фиксированные высоты, отключённыйoutline, текст картинкой. Каждый шаг ломает доступность ради совпадения. - Рендеринг всё равно разный. Хинтинг шрифтов, субпиксельное сглаживание, системный масштаб, настройка минимального размера шрифта в браузере. Пиксельное совпадение недостижимо в принципе.
- Оно съедает бюджет ревью. Час, потраченный на спор о 2 px внутреннего отступа, — это час, не потраченный на пустое состояние и на проверку клавиатурой.
Что вместо — договоритесь о двух списках. Нерушимое — токены и шкалы (шаг сетки, размеры текста, цвета из палитры), иерархия и порядок, формулировки, размеры зон нажатия, поведение состояний. Отклонение здесь — дефект.
Договорное — точные отступы в пределах шкалы, перенос строк, тонкая настройка теней, поведение на промежуточных ширинах. Отклонение здесь — повод обсудить, а не завести баг.
И обратное обязательство дизайнера: проверять сборку, а не картинку. Пройти форму с клавиатуры, включить зум 200%, посмотреть в градациях серого. Пятнадцать минут в ветке до релиза заменяют месяц переписки после. Как это встроить в процесс команды — в https://courses.digitable.life/post/ux-design/13-handoff-and-career/.
Чек-лист ревью макета
Типичные ошибки
- Контраст «на глаз». Дизайнер смотрит на калиброванный экран в тени и уверен, что читается. Считайте числом — это быстрее, чем спорить.
- Проверка контраста только для покоя.
hoverиactiveчасто светлее и вылетают за порог. - Отсутствие фокуса в макете. Потом это превращается в
outline: noneв коде, и разработчик формально прав: он сделал по макету. - Иерархия текста без имён уровней. Разработчик угадывает, получается страница из шести
h1. - Раскладка, ломающая порядок чтения. Колонки, «карточка справа, а по смыслу первая», визуальные перестановки ради красоты.
- Только два брейкпоинта. Между 375 и 1440 живёт большинство: планшеты, окна в половину экрана, десктоп с зумом.
- Тексты-заглушки. «Lorem ipsum» скрывает, что реальное название не влезет в одну строку.
- Доступность как финальная стадия. Проверка перед релизом находит то, что уже нельзя поменять дёшево; проверка на вайрфрейме (https://courses.digitable.life/post/ux-design/05-wireframes-and-prototypes/) стоит копейки.
- Аудит вместо процесса. Разовый отчёт на 200 пунктов деградирует за квартал, если правила не встроены в дизайн-систему и в ревью.
Мини-итог
- Значительная часть барьеров создаётся в макете. Их не починить в коде без переделки макета.
- Контраст считается, а не подбирается: пороги 4.5:1 для текста, 3:1 для крупного текста, границ и индикатора фокуса. Тёмная тема считается отдельно.
- Зона нажатия — не то же самое, что видимый глиф. Минимум 24 × 24, комфорт 44 × 44, зазор 8.
- Состояние, которого нет в макете, в продукте будет случайным. Фокус — обязательное состояние, а не украшение.
- Один осмысленный линейный порядок чтения — свойство хорошей раскладки; если его нет, проблема в структуре, а не в ARIA. Разделяйте «есть порог» и «мне нравится»: первое — требование, второе — предмет договорённости.
- Нормальная передача — это состояния, правила, токены и аннотации, а не набор картинок. «Пиксель в пиксель» — не цель, а способ потратить бюджет качества не туда.
Источники
- WCAG 2.2 и Understanding WCAG 2.2 — формулировки критериев и, что важнее, объяснения «почему».
- WebAIM Million — ежегодная статистика реальных ошибок.
- Apple Human Interface Guidelines: Accessibility.
- Microsoft Inclusive Design Toolkit.
- APCA — перспективная модель контраста для WCAG 3.
- Adam Silver, «Form Design Patterns» — разбор форм по шагам.
- Heydon Pickering, «Inclusive Components» — компоненты с точки зрения поведения, полезно читать вместе с разработчиком.
- NN/g: Fitts’s Law, 10 эвристик Нильсена и про плейсхолдеры.
Что дальше
Числовые пороги проверяются линейкой и калькулятором, а всё остальное — только на людях. Дальше разбираемся, как проверить макет на реальных пользователях и не обмануть себя выборкой, формулировкой заданий и собственными ожиданиями: Юзабилити-тестирование: как проверить макет на людях и не обмануть себя.