Перейти к содержимому
EN
Масштаб
Плотность
Тема
Акцент

О проекте

Проверка по пикселям

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