UX и проектирование интерфейсов Доступность на этапе дизайна: контраст, размеры, состояния, порядок
0%

Доступность на этапе дизайна: контраст, размеры, состояния, порядок

Доступность на этапе дизайна: контраст, размеры, состояния, порядок

Типичная сцена. Команда решила «заняться доступностью». Разработчик проходится по компонентам, расставляет aria-label, чинит роли, добавляет role="dialog" и ловушку фокуса в модалке. Автотест на axe становится зелёным. А потом приходит пользователь, у которого возрастная дальнозоркость и телефон на солнце, и он не видит подпись под полем, не находит кнопку «Оплатить» и трижды промахивается мимо иконки удаления.

Ничего удивительного: барьеры, которые он встретил, были заложены не в коде — они были нарисованы. Серый #9aa0a6 на белом фоне, иконка 16 × 16 без отступов, отсутствие состояния фокуса в макете (его просто никто не отрисовал), кнопка-призрак с границей контрастом 1.6:1 — это решения дизайна. Разработчик может либо повторить их «пиксель в пиксель», либо начать спор.

Эта статья — про ту часть доступности, которая решается до кода. Полная картина — стандарты, ARIA, скринридеры, тестирование — разобрана в отдельном треке: начните с обзора трека «Доступность». Здесь — только то, что дизайнер обязан решить в макете, и то, как это передать в разработку, чтобы решение выжило.

Что решается в макете, а что в коде

Полезно провести границу явно — иначе начинается перекладывание ответственности.

Решение Кто закладывает Где ломается
Контраст текста и границ дизайн палитра, токены
Размер и отступы зон нажатия дизайн компонент, плотные таблицы
Состояния: hover, focus, disabled, loading, error дизайн «не нарисовали — не сделали»
Иерархия заголовков дизайн стили текста без имён уровней
Порядок чтения и фокуса дизайн (структура) → код (разметка) двухколоночные раскладки
Текст лейблов, ошибок, кнопок дизайн + редактор плейсхолдеры вместо лейблов
Роли, семантика, ARIA код div вместо button
Управление фокусом при переходах код (по сценарию дизайна) модалки, роутинг

Главная мысль: у части дизайнерских решений есть числовой критерий, проверяемый за минуту в макете. Спорить о них после релиза — дорого и бессмысленно.

Для кого мы это делаем

Разговор о доступности часто буксует, потому что в голове возникает образ «слепого пользователя» и оценка «их же мало». Полезнее модель 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. Текст исчезает при любом внешнем свете.

Правильный разбор — не «жёлтый плохой», а «жёлтый светлый», а значит:

  1. Жёлтый — это фон под тёмным текстом, а не под светлым. #1A1A1A на #FFC400 даёт около 12:1.
  2. Если бренд требует жёлтые буквы, они живут на тёмном фоне, а не на белом.
  3. Для ссылок и мелкого текста жёлтый не подходит вовсе: нужен затемнённый вариант того же тона.

Это типовой ответ на «нам нельзя менять фирменный цвет»: цвет не меняется, меняется его роль. Роли — это уже работа с токенами, см. 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. Жалобы: «удалил не ту запись». Разбираем по слоям:

  1. Моторика. Пятно контакта пальца — около 10 мм, это примерно 38 CSS px. Две цели по 16 px с зазором 4 px помещаются внутрь одного касания целиком. Промах не вероятность, а норма.
  2. Цена ошибки. Соседство «безопасного» и «разрушительного» действия — отдельная ошибка дизайна. Действия с разной ценой не ставят рядом одинаковыми по весу.
  3. Обратимость. Если удаление необратимо, отсутствие отмены — третья ошибка.

Как чинится в макете:

  • Зона нажатия каждой иконки — 32 × 32 минимум (в плотных таблицах это компромисс между 24 и 44), зазор между зонами 8 px.
  • «Удалить» уезжает в меню действий либо получает подтверждение; на экране остаётся только частое безопасное действие.
  • Появляется тост «Запись удалена. Отменить» с окном 5–10 секунд. Отмена дешевле подтверждения: она не тормозит 99% успешных сценариев.

Обратите внимание: из трёх правок только первая — про доступность в узком смысле. Остальные две просто делают интерфейс лучше для всех. Это общее свойство: доступность редко бывает отдельной работой, чаще это дисциплина принятия обычных решений.

Состояния: то, чего нет в макете, не будет в продукте

Самая дешёвая и самая частая дизайнерская ошибка — отрисовать компонент в одном состоянии. Дальше разработчик выдумывает остальные, а «выдумывает» на практике значит «берёт дефолт браузера или отключает его».

Минимальный набор, который дизайнер отдаёт для любого интерактивного элемента: покой, наведение, фокус, нажатие, отключено, загрузка, ошибка, успех, а для контейнеров — ещё пустое состояние и состояние «данные не загрузились». Подробнее про сами состояния и обратную связь — https://courses.digitable.life/post/ux-design/08-interaction/.

Фокус — не украшение, а навигация

Индикатор фокуса — единственный способ понять, где ты находишься, если ты не пользуешься мышью. Мышью не пользуются не только незрячие: клавиатура быстрее при заполнении форм, у части людей тремор или травма, а на телевизорах и в автомобилях мыши нет вовсе. Что должно быть в макете:

  • Стиль кольца фокуса как отдельный токен: цвет, толщина (обычно 2 px), отступ от границы (offset 2 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, пока не заполнены все поля. Что не так:

  • Отключённый элемент по умолчанию не получает фокус — клавиатурный пользователь не может даже дойти до него и понять, что кнопка есть.
  • Он не объясняет, чего не хватает. Пользователь смотрит на серую кнопку и гадает.
  • Он рисуется бледным, и на исключение из требований контраста ссылаются как на индульгенцию — а человек всё равно должен прочитать его текст.
  • Скринридер часто просто пропускает такой элемент при навигации по кнопкам.

Как лучше: кнопка активна всегда. При нажатии с незаполненными полями — валидация, сводка ошибок над формой и перевод фокуса на первое проблемное поле. Пользователь получает ответ на вопрос «почему не работает», а не тупик. Если отключение всё же обязательно (действие недоступно по правам), рядом должно быть объяснение текстом, а не только тултип по наведению.

Порядок: визуальный, логический, фокусный

Есть три порядка, и они должны совпадать:

  1. Визуальный — как глаз сканирует экран.
  2. Порядок чтения — в каком порядке контент идёт в разметке (его слышит скринридер).
  3. Порядок фокуса — как Tab обходит интерактивные элементы.

Расхождение между ними — почти всегда следствие раскладки, придуманной в макете.

Порядок чтения и фокуса в двухколоночной форме

Разбор левой части схемы. Дизайнер собрал форму как «две колонки», потому что так красивее по сетке. Разработчик честно сверстал два вертикальных контейнера. Итог: Tab идёт «Имя → Город → Телефон → Фамилия → Индекс → E-mail». Зрячий пользователь мыши этого не заметит. Клавиатурный потеряется, а скринридер прочитает адрес вперемешку с контактами.

Чинится не в CSS. Порядок должен быть заложен структурой: сетка собирается по строкам, а пары полей, которые заполняются вместе, живут в одной строке. Побочный выигрыш: при сужении до мобильной ширины колонки схлопываются, и порядок не меняется.

Правило, которое стоит повесить над столом: макет обязан иметь один осмысленный линейный порядок. Если вы не можете прочитать экран сверху вниз одним потоком и он остаётся понятным — экран спроектирован неправильно, а не «просто нуждается в ARIA».

Заголовки: иерархия текста — это структура документа

Скринридер умеет обходить страницу по заголовкам, и для многих пользователей это основной способ навигации. Уровни заголовков берутся из макета: то, что дизайнер называет «Title / Section / Subsection», в коде становится h1h3. Что делать дизайнеру:

  • Называть стили текста по семантике, а не по размеру. 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. Разберём, из чего состоит нормальная передача.

Что входит в передачу

  1. Все состояния каждого компонента, а не «главный экран». Отдельно — пустое состояние, состояние ошибки загрузки, состояние с длинным текстом и состояние «одна запись».
  2. Правила поведения, а не набор ширин. Что сжимается, что переносится, что обрезается многоточием, что скрывается, минимальная и максимальная ширина колонок. Одна страница текста заменяет пять артбордов.
  3. Токены вместо значений. color/text/secondary, а не #6E6E6E. Это единственный способ не потерять контраст при смене темы; см. https://courses.digitable.life/post/ux-design/07-design-systems/.
  4. Аннотации доступности — отдельный слой в макете: пронумерованный порядок фокуса; уровень каждого заголовка; alt-тексты для содержательных изображений и пометка «декоративное» для остальных; подписи иконочных кнопок (они станут доступным именем); что и каким текстом объявляется при асинхронных изменениях; что делают Esc, Enter и стрелки в кастомных виджетах.
  5. Реальные тексты — длинные, пустые, на второй локали — и список открытого: что можно менять при реализации, а что нерушимо.

Формат аннотаций может быть любым — метки в 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/.

Чек-лист ревью макета

Типичные ошибки

  1. Контраст «на глаз». Дизайнер смотрит на калиброванный экран в тени и уверен, что читается. Считайте числом — это быстрее, чем спорить.
  2. Проверка контраста только для покоя. hover и active часто светлее и вылетают за порог.
  3. Отсутствие фокуса в макете. Потом это превращается в outline: none в коде, и разработчик формально прав: он сделал по макету.
  4. Иерархия текста без имён уровней. Разработчик угадывает, получается страница из шести h1.
  5. Раскладка, ломающая порядок чтения. Колонки, «карточка справа, а по смыслу первая», визуальные перестановки ради красоты.
  6. Только два брейкпоинта. Между 375 и 1440 живёт большинство: планшеты, окна в половину экрана, десктоп с зумом.
  7. Тексты-заглушки. «Lorem ipsum» скрывает, что реальное название не влезет в одну строку.
  8. Доступность как финальная стадия. Проверка перед релизом находит то, что уже нельзя поменять дёшево; проверка на вайрфрейме (https://courses.digitable.life/post/ux-design/05-wireframes-and-prototypes/) стоит копейки.
  9. Аудит вместо процесса. Разовый отчёт на 200 пунктов деградирует за квартал, если правила не встроены в дизайн-систему и в ревью.

Мини-итог

  • Значительная часть барьеров создаётся в макете. Их не починить в коде без переделки макета.
  • Контраст считается, а не подбирается: пороги 4.5:1 для текста, 3:1 для крупного текста, границ и индикатора фокуса. Тёмная тема считается отдельно.
  • Зона нажатия — не то же самое, что видимый глиф. Минимум 24 × 24, комфорт 44 × 44, зазор 8.
  • Состояние, которого нет в макете, в продукте будет случайным. Фокус — обязательное состояние, а не украшение.
  • Один осмысленный линейный порядок чтения — свойство хорошей раскладки; если его нет, проблема в структуре, а не в ARIA. Разделяйте «есть порог» и «мне нравится»: первое — требование, второе — предмет договорённости.
  • Нормальная передача — это состояния, правила, токены и аннотации, а не набор картинок. «Пиксель в пиксель» — не цель, а способ потратить бюджет качества не туда.

Источники

Что дальше

Числовые пороги проверяются линейкой и калькулятором, а всё остальное — только на людях. Дальше разбираемся, как проверить макет на реальных пользователях и не обмануть себя выборкой, формулировкой заданий и собственными ожиданиями: Юзабилити-тестирование: как проверить макет на людях и не обмануть себя.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов