Цвет, контраст и движение
Цвет — самый ненадёжный канал передачи смысла: его по-разному видят сотни миллионов человек, а на солнце его не видит вообще никто.
Контраст — числовое отношение яркости текста к яркости фона: от 1:1 (невидимо) до 21:1 (чистый чёрный на чистом белом). Для обычного текста WCAG требует минимум 4.5:1.
Зачем это на практике
Низкий контраст — самый массовый дефект доступности в вебе: ежегодное исследование WebAIM Million годами находит нечитаемый текст примерно на восьми из десяти главных страниц. И это не про экзотику. Это про:
- ~8% мужчин и ~0,5% женщин с нарушением цветовосприятия (около 300 млн человек в мире);
- возрастное помутнение хрусталика — после 50 лет контрастная чувствительность падает у всех;
- телефон на улице в солнечный день — временная слепота на бледный текст;
- дешёвый монитор в офисе, режим энергосбережения, ночной фильтр синего;
- просто усталые глаза в конце рабочего дня.
Контраст — тот редкий случай, когда доступность буквально измеряется числом. Не надо спорить с дизайнером «мне кажется, светловато» — есть формула.
Контраст: цифры, которые надо знать
| Что | Минимум (уровень AA) | Повышенный (AAA) |
| Обычный текст (меньше 24px, или меньше 18.66px жирного) | 4.5:1 | 7:1 |
| Крупный текст (от 24px, или от 18.66px жирного) | 3:1 | 4.5:1 |
| Границы полей, иконки, элементы управления | 3:1 | — |
| Логотипы, чисто декоративный текст | требований нет | — |
Смотрите, как мало нужно, чтобы промахнуться:
/* Плохо: 2.6:1 — не проходит даже близко */
.hint { color: #9aa0a6; background: #ffffff; }
/* Хорошо: 4.5:1 — минимально допустимый серый на белом */
.hint { color: #767676; background: #ffffff; }
На глаз разница невелика. Для человека с возрастным снижением зрения — это разница между «читаю» и «не читаю».
Где контраст проваливают чаще всего
- Плейсхолдеры. Браузерный серый по умолчанию балансирует на грани — а его ещё и осветляют «для красоты».
- Белый текст на брендовой кнопке. Классика жанра: жёлтый
#FFC107+ белый текст даёт 1.6:1 — практически нечитаемо. А чёрный на том же жёлтом — 12.9:1. Иногда доступность требует просто поменять цвет текста. - Текст поверх фотографии. Контраст «плавает» по площади: над тёмным углом всё отлично, над облаком — ничего не видно. Лечится полупрозрачной подложкой или градиентом, а не надеждой.
- Ссылка в абзаце, отличающаяся только цветом — про это ниже, это отдельный критерий.
- Тёмная тема. Отдельная проверка, а не «инвертировали и поехали». Чисто белый
#fffна чисто чёрном#000даёт формальные 21:1 — и эффект «гало»: буквы расплываются, особенно при астигматизме. Мягче и правильнее:#e8e8e8на#121212— это 15:1 и никакого свечения.
Считать контраст на глаз нельзя. Используйте контраст-чекер (WebAIM Contrast Checker), встроенную пипетку DevTools (она показывает отношение прямо в палитре) или axe/Lighthouse — они прогонят всю страницу. Считать нужно на этапе дизайна: перекрашивать готовый интерфейс дороже, чем поправить токены в макете.
Смысл только цветом — так нельзя
Критерий WCAG 1.4.1 звучит так: цвет не должен быть единственным средством передачи информации, обозначения действия или различения элементов. Вот типичные нарушения — почти наверняка вы их видели на этой неделе:
- «Поля, выделенные красным, обязательны для заполнения»;
- красная рамка вокруг поля — и ни слова текста;
- легенда графика: «синяя линия — 2024, оранжевая — 2025»;
- статусы заявок в таблице: цветные кружки без подписи;
- ссылка внутри текста, которая отличается от обычного текста только цветом.
Последний пункт коварнее всех. Возьмите два цвета, которые для зрячего различаются как небо и земля: красный #FF0000 и зелёный #00A000. Их контраст друг с другом — примерно 1.15:1. Для человека с дейтеранопией они сливаются в один тон. Именно поэтому WCAG требует, чтобы ссылка либо была подчёркнута, либо контрастировала с окружающим текстом минимум на 3:1.
Как чинить: добавьте второй канал
Цвет оставляем — он полезен и приятен. Просто дублируем его чем-то ещё:
- словом («Отклонено», «Ошибка:», «Готово»);
- иконкой с текстовой альтернативой (крестик, галочка, восклицательный знак);
- подчёркиванием — для ссылок в тексте;
- штриховкой и формой — на графиках: разные типы линий, разные маркеры, подписи прямо у линий вместо легенды сбоку.
<p class="status status--error">
<svg aria-hidden="true" focusable="false" width="16" height="16"></svg>
<strong>Отклонено:</strong> не хватает документов
</p>
Красный тут остаётся, но он больше не несёт смысл в одиночку: смысл несёт слово «Отклонено».
.article a {
color: #0b57d0;
text-decoration: underline;
}
/* Никогда: outline: none; — это отключение клавиатуры */
.article a:focus-visible {
outline: 3px solid #0b57d0;
outline-offset: 2px;
}
outline: none — самый вредный однострочник в истории CSS. Мышью всё работает, поэтому автор правки ничего не замечает; а человек, который ходит по сайту клавиатурой, теряет курсор и не понимает, где находится.
Проверка за 10 секунд
В Chrome DevTools откройте панель Rendering → Emulate vision deficiencies и включите achromatopsia (полное отсутствие цветовосприятия). Если в чёрно-белом интерфейс остаётся понятным — вы использовали цвет как усиление. Если статусы, ошибки и ссылки исчезли — цвет у вас несёт смысл в одиночку, и это надо чинить.
Движение: анимации, от которых тошнит
Это не преувеличение и не про «кому-то не нравится». У людей с вестибулярными расстройствами (после сотрясения, при мигрени, при болезни Меньера) крупная анимация вызывает настоящее головокружение и тошноту — иногда на несколько часов. Критерий WCAG 2.3.3 существует именно поэтому.
Опасно то движение, которое занимает большую площадь экрана и создаёт ощущение, что двигается сам мир:
- параллакс при прокрутке (слои едут с разной скоростью);
- зум-переходы между экранами, «влетающие» и «улетающие» модалки;
- вращения, спирали, большие смещения;
- бесконечные автокарусели и бегущие строки;
- фоновое видео с движением камеры.
Относительно безопасны: изменение прозрачности (fade), плавная смена цвета, короткие сдвиги на несколько пикселей.
prefers-reduced-motion
В системе есть переключатель «Уменьшить движение» (macOS: Универсальный доступ → Дисплей; Windows: Специальные возможности → Визуальные эффекты; в Android и iOS аналогично). Браузер сообщает о нём CSS медиазапросом — и это единственный правильный способ узнать, что человеку нужен спокойный интерфейс.
.card { transition: transform 300ms ease; }
.card:hover { transform: translateY(-8px) scale(1.04); }
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover { transform: none; }
}
Общий «предохранитель» на весь проект — страховка от того, что кто-то забудет:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Обратите внимание: 0.01ms, а не 0s. Нулевая длительность в части браузеров не порождает событие animationend — и логика на JS, которая его ждёт, зависает навсегда. Микроскопическая длительность и движение убирает, и событие сохраняет.
Но выключать всё подряд — грубо. Правильнее заменять движение на спокойный эквивалент:
@media (prefers-reduced-motion: no-preference) {
.modal { animation: slide-up 250ms ease-out; }
}
@media (prefers-reduced-motion: reduce) {
.modal { animation: fade-in 150ms linear; }
}
Здесь важен приём: анимацию мы включаем внутри no-preference. Тогда «без движения» становится состоянием по умолчанию, и забытый уголок интерфейса не превратится в проблему.
Если анимацию рисует JavaScript (canvas, WebGL, конфетти после покупки), тот же флаг читается из скрипта:
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduceMotion) {
showStaticSuccessBadge();
} else {
launchConfetti();
}
Ещё три правила про движение
- Мигание — не чаще 3 раз в секунду (WCAG 2.3.1). Более частое мигание — реальный риск фотосенситивного эпилептического приступа. Это одно из немногих требований, нарушение которого может отправить человека в больницу.
- Всё, что движется дольше 5 секунд, должно иметь паузу (WCAG 2.2.2): карусель, бегущая строка, автообновляемая лента. Кнопка «Пауза» — обязательна, и не в подвале, а рядом.
- Автокарусель на главной — почти всегда плохая идея. Она мешает и человеку с СДВГ, и тому, кто просто читает медленнее, чем вы задали таймер.
Как это работает
Контраст в WCAG считается по формуле (L1 + 0.05) / (L2 + 0.05), где L — относительная яркость (relative luminance) более светлого и более тёмного цвета. И вот главное: яркость — это не среднее арифметическое каналов RGB. Каналы берутся с весами, отражающими чувствительность человеческого глаза:
L = 0.2126 * R + 0.7152 * G + 0.0722 * B
(~21%) (~72%) (~7%)
Зелёный даёт почти три четверти воспринимаемой яркости, синий — почти ничего. Отсюда следуют вещи, которые сначала кажутся парадоксом:
- Жёлтый почти не контрастирует с белым (в нём много красного и зелёного — он «яркий»), зато отлично работает с чёрным.
- Насыщенно-синий на белом читается прекрасно, хотя «на глаз» кажется не таким уж тёмным: синий канал почти не добавляет яркости.
- Разные цвета ≠ контрастные цвета. Тот самый красный и зелёный с контрастом 1.15:1 — разные до противоположности, но по яркости почти одинаковые.
Отсюда же ответ на вечный вопрос «а почему нельзя просто взять контрастную палитру и не думать?». Потому что контраст — это про яркость, а различимость для дальтоника — про тон. Это две разные оси, и проходить надо по обеим: сначала измерить контраст, потом посмотреть на интерфейс в чёрно-белом.
Частые ошибки
| Ошибка | Чем плоха | Как правильно |
| Светло-серый текст «для воздуха» | 2.5–3:1, не читается на солнце и после 50 лет | минимум 4.5:1, проверять чекером |
| Белый текст на жёлтой/светло-зелёной кнопке | около 1.6:1 — надпись сливается | тёмный текст на светлом фирменном цвете |
| «Обязательные поля выделены красным» | смысл только цветом (WCAG 1.4.1) | слово, звёздочка с расшифровкой, required |
| Ссылка в тексте без подчёркивания | дальтоник её не отличит от текста | подчёркивание или контраст 3:1 с текстом |
outline: none на фокусе | клавиатурный пользователь теряет курсор | заметный :focus-visible |
| Инвертировали цвета и назвали это тёмной темой | #fff на #000 даёт «гало», текст расплывается | #e8e8e8 на #121212, контраст мерить заново |
Параллакс и зум без prefers-reduced-motion | вызывает головокружение и тошноту | заменить на fade в ветке reduce |
animation-duration: 0s в reduce-ветке | событие animationend может не сработать | 0.01ms |
| Автокарусель без паузы | нарушает WCAG 2.2.2 | кнопка «Пауза» или отказ от карусели |
Итоги
- Контраст — единственная часть доступности, которая измеряется числом: 4.5:1 для обычного текста, 3:1 для крупного и для элементов управления.
- Яркость считается с весами (зелёный ~72%, синий ~7%). Поэтому жёлтый плохо дружит с белым, а «разные цвета» вовсе не значит «контрастные».
- Цвет никогда не должен быть единственным носителем смысла: дублируйте словом, иконкой, подчёркиванием, формой.
- Быстрая проверка: включите в DevTools ахроматопсию. Понятно в чёрно-белом — значит, всё хорошо.
- Крупное движение (параллакс, зум, вращение) реально вызывает тошноту. Уважайте
prefers-reduced-motion— и лучше включайте анимации внутриno-preference, а не выключайте внутриreduce. - Ничего не мигает чаще 3 раз в секунду; всё, что движется дольше 5 секунд, можно остановить.