О проекте
Проверка по пикселям
tools/audit.js меряет контраст и цели нажатия по тому, что нарисовано в
браузере. Это дополнение к текстовым проверкам: они разбирают tokens.css,
страницы документации и собранные файлы, но между объявлением токена и пикселем
стоят вложенность, полупрозрачные слои, каскад приложения и выбранная тема.
Проверка обходит каждый узел с текстом, берёт его настоящий цвет и настоящий
фон, собирая альфу по всем предкам, и считает контраст. Цвет в любой записи —
oklch, color-mix, light-dark — приводит к sRGB сам браузер: значение
рисуется на канве размером в пиксель и читается обратно.
Запускать её нужно на своём экране: список пар для текстовой проверки составить заранее нельзя, потому что число сочетаний зависит от того, как вложены слои в конкретном приложении.
Как запустить
Откройте свой экран, вставьте tools/audit.js
в консоль браузера и вызовите:
kitAudit.run() // вся страница: все оси разом
kitAudit.run('#main') // только часть
kitAudit.contrast() // один прогон в текущих теме и акценте
kitAudit.targets() // один прогон в текущих масштабе и плотности
kitAudit.proportion() // там же: влезает ли текст в свою коробку
run() печатает сводку таблицей и раскрывает нарушения по ячейкам. Их же
возвращает объектом, если результат нужно разобрать программой.
Оси перемножаются не все со всеми, и это осознанно. Контраст идёт по теме ×
акценту: акцент переопределяет рампу --a-*, а подпись на акцентной заливке
в каждой теме своя. Цели и пропорции — по масштабу × плотности: обе оси
двигают геометрию и обе округляют до целых пикселей независимо друг от друга.
Умножать одно на другое значило бы удлинить прогон двадцатикратно ради
повторения тех же чисел.
Что она меряет
| Контраст текста | 4.5:1, а для крупного (24px или 18.66px полужирным) — 3:1. Фон собирается по предкам, полупрозрачные слои складываются в порядке рисования |
| Цели нажатия | 24×24 по WCAG 2.2 AA (2.5.8) с двумя исключениями критерия: по расстоянию — цель меньше нормы засчитывается при S + G ≥ 24; инлайновая — ссылка внутри строки текста освобождена совсем |
| Пропорции | Влезает ли подпись в свою колонку и не перенеслась ли она на вторую строку; чернила значка против высоты прописной соседней подписи. Это единственное, что нельзя вывести из токенов: ширина строки зависит от гарнитуры, начертания и языка |
| Темы и акценты | 5 × 4 для контраста. Тема, поставленная на поддереве, при этом остаётся своей: корневая её не перекрывает |
| Масштабы и плотности | 5 × 3 для целей и пропорций |
Чего она не видит
| Скрытое | Закрытый поповер, свёрнутая панель, вторая вкладка — их нет в раскладке. Откройте и прогоните ещё раз |
| Фон картинкой | Считается только цвет. Текст поверх изображения проверка пропустит |
| Порядок и смысл | Она измеряет пиксели. Клавиатура, имена ролей и порядок чтения остаются за человеком |
| Состояния | Наведение, фокус и нажатие живут в момент взаимодействия. Меряется то, что видно сейчас |
Когда результату не стоит верить
Результат зависит от момента замера. Пять случаев дают ложные нарушения.
Замер во время анимации. Смена темы анимирована, и цвет, прочитанный в том
же кадре, — это интерполяция. Признак: браузер возвращает цвет как oklab(…)
вместо oklch(…). run() глушит переходы сам, contrast() — нет.
Дробные ширины. Кнопка сегментированного контрола в плотном режиме — это 22px высоты и 2px зазора, ровно 24. При дробной ширине браузер отдаёт зазор 1,9999, и норма формально падает. Допуск в 0,1px это гасит; запаса там нет, и приложение, которое чуть сожмёт зазор, сломает WCAG молча.
Область нажатия, выращенная псевдоэлементом. Мелкие контролы остаются
мелкими (глиф флажка 13px), а до нормы их растит невидимый ::before. Проверка
берёт наибольшее из элемента и его абсолютно позиционированных
псевдоэлементов; инлайновый псевдоэлемент украшает, а не расширяет цель.
Липкая шапка над прокруткой. Расстояние меряется в координатах окна, и элемент, проезжающий над другим, читается как стоящий вплотную. Пересекающиеся коробки — это слои, а не соседи, и проверка их не сравнивает.
Видимо скрытые контролы. Ссылка «Перейти к содержимому» и подписи для
скринридеров живут коробкой в пиксель с clip-path, а настоящей целью
становятся только в фокусе. Признак — обрезка clip-path: inset(50%), а не
размер: элемент с border-box не может стать тоньше своих рамок, поэтому
объявленный 1px рисуется как 2px.
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument