Аудит доступности сайта: что уже работает и что стоит поправить

Дата аудита: 18 августа 2026 · Стандарты: WCAG 2.2 (уровень AA, местами AAA), WAI-ARIA 1.2, ГОСТ Р 52872-2019 Область: публичная часть, личный кабинет, рабочее место библиотекаря — 154 компонента и страницы Статус: аудит проведён, план работ выполнен — см. блок «Что исправлено» ниже
Коротко о результате
Сайт заметно выше среднего по доступности. Здесь не «прикрутили ARIA перед сдачей» — доступность заложена в архитектуру: настройки читателя живут в cookie и применяются классами на <html> ещё на сервере, поэтому первый же кадр страницы приходит уже в нужной схеме, без мигания. Графики сопровождаются обязательной таблицей. Ссылки в текстах сами сообщают скрин-ридеру «открывается в новом окне». Каталог книг размечен как полноценный комбобокс с живыми областями.
Основные проблемы лежат не в разметке, а в цветовых токенах и в одном месте с неверными ARIA-ролями. Причём самые серьёзные из них проявляются именно в тёмных и высококонтрастных схемах — то есть у той части читателей, ради которой сайт и делается.
Методика
Проверка велась по исходному коду, без запуска браузера:
- Контраст. Все 14 цветовых схем (5 фонов × светлый и тёмный режим = 10 обычных, плюс 4 высококонтрастные; с учётом 5 акцентных цветов) разобраны на пары «текст ↔ фон», «граница ↔ фон», «акцент ↔ подложка». Коэффициенты посчитаны по формуле относительной яркости WCAG 2.x. Для полупрозрачных подложек (
bg-*-950/30) выполнено альфа-смешивание с фактическим фоном схемы. - Семантика и ARIA. Разбор всех открывающих тегов (включая многострочные) в 154
.vue-файлах: доступные имена, роли, состояния, порядок заголовков, ориентиры страницы. - Клавиатура. Поиск обработчиков
keydown, управленияtabindex, ловушек фокуса — сопоставление объявленных ARIA-ролей с фактической клавиатурной моделью. - Формы, таблицы, медиа. Покрытие подписями,
scope/caption,autocomplete,titleуiframe,altу изображений.
Что уже сделано хорошо
Это стоит зафиксировать: перечисленное ниже не нужно трогать, и на это можно опираться при доработках.
Основа страницы. lang="ru" на <html>. Skip-ссылка не просто перебрасывает якорь, а переводит фокус на <h1> — скрин-ридер сразу называет страницу, и следующий Tab продолжает обход с содержимого, а не с начала документа. Есть постоянная область aria-live="polite" для объявлений.
Настройки доступности. Восемь параметров: тема, акцент, фон, кегль, полужирный, гарнитура, выключка, переносы, плюс отдельный масштаб текста материала. Все изменения озвучиваются — вызовы announceChange() стоят на каждой из восьми настроек, с человеческими формулировками («Текст выровнен по ширине», «Переносы слов включены»). Это редкость: обычно панель доступности молча меняет вид.
Четыре высококонтрастные схемы — чёрное на белом, белое на чёрном, жёлтое на синем, зелёное на чёрном. Все четыре дают контраст основного текста от 7,18:1 до 21,00:1, то есть уровень AAA с запасом.
Типографика. База 18px (не 16), межстрочный 1,6, межбуквенный 0,012em, ширина строки ограничена 75 символами, отступ между абзацами 1,5em. Размеры заданы в rem, поэтому масштабирование до 200% не ломает раскладку. Выключка по формату с переносами включена по умолчанию и отключается читателем — это правильный компромисс между красотой для зрячих и удобством для тех, кому «реки» в тексте мешают.
Компонент ссылок в текстах (ProseA.vue) — образцовый: подчёркивание, увеличенное расстояние до текста, иконка типа ссылки, скрытая подпись «(открывается в новом окне)», «(скачать файл)», «(позвонить)», видимое кольцо фокуса.
Компонент графиков (DataChart.vue) — тоже образцовый: сам график помечен aria-hidden, под ним всегда таблица с теми же числами, значения подписаны у каждой полосы, полосы построены из HTML-блоков, а не из SVG, поэтому растут вместе с кеглем.
Каталог книг — самая сложная страница сайта — размечен аккуратнее многих коммерческих: живые области для числа результатов, role="status" у подсказок, группы подсказок связаны через aria-labelledby, у каждого фильтра своё доступное имя.
Гигиена разметки. Ни одного кликабельного <div> или <span> без роли. Ни одного положительного tabindex. Все iframe с видео имеют title. Из сотен кнопок только 4 иконочные остались без доступного имени. Уважается prefers-reduced-motion.
Критично
1. Навигация по разделам объявлена меню, но им не является
SectionNav.vue — боковая навигация по разделам сайта. Список размечен как role="menubar", вложенные — role="menu", а сами ссылки — role="menuitem".
При этом в компоненте нет ни одного обработчика клавиатуры: ни keydown, ни управления tabindex, ни обработки Escape. Подменю, по комментарию в коде, «ВСЕГДА ВИДИМЫ» — то есть это обычное дерево ссылок, а не раскрывающееся меню.
Последствия для читателя со скрин-ридером:
- NVDA и JAWS, встретив
menubar, переключаются в режим приложения и отдают стрелки виджету. Виджет их не обрабатывает — пользователь оказывается в ловушке: привычная навигация стрелками по строкам перестаёт работать, а обещанная навигация по меню не начинает. role="menuitem"стирает у ссылки её природу: вместо «ссылка, Деятельность» читатель слышит «пункт меню, Деятельность» и не понимает, что переход уведёт со страницы.role="menubar"на<ul>снимает семантику списка — теряется объявление «список из 9 элементов», по которому ориентируются в структуре.
Нарушено: WCAG 4.1.2 «Имя, роль, значение» (уровень A) — роль обещает модель взаимодействия, которой нет.
Рекомендация. Снять все роли menubar / menu / menuitem / none. Оставить <nav aria-label="Разделы сайта">, обычные <ul> / <li> / <a> и уже имеющийся aria-current="page". Роли меню в WAI-ARIA Authoring Practices предназначены для меню приложений (как «Файл → Правка»), а не для навигации по сайту — это прямо оговорено в спецификации. После снятия ролей навигация станет доступнее без единой строки нового кода.
2. Skip-ссылка нечитаема в тёмных и высококонтрастных схемах
В app.vue и в main.css у ссылки «Перейти к основному содержимому» задано:
background-color: var(--ui-primary);
color: white;
Цвет текста зафиксирован белым, а фон следует за акцентом темы. В светлых темах акцент — тёмный оттенок (green-700), и всё в порядке. Но в тёмных темах акцент светлый (green-400 = #00DC82), а в режиме высокого контраста --ui-primary совпадает с цветом текста схемы.
| Схема оформления | Значение, :1 |
|---|---|
| Светлая, акцент зелёный Норма выполнена | 5,1 |
| Светлая, акцент жёлтый Норма выполнена, запас минимальный | 4,92 |
| ВК «чёрное на белом» Норма выполнена | 21 |
| Тёмная, акцент зелёный Нечитаемо | 1,82 |
| Тёмная, акцент жёлтый Нечитаемо | 1,53 |
| ВК «жёлтое на синем» Нечитаемо | 1,51 |
| ВК «терминал» Нечитаемо | 1,37 |
| ВК «белое на чёрном» Белое по белому — ссылка не видна вовсе | 1 |
Источник: Норма WCAG 1.4.3 (AA) — 4,5:1. Расчёт по CSS-переменным main.css
Отдельно отмечу: включение режима высокого контраста принудительно ставит colorMode.preference = 'dark'. Значит, любой пользователь высокого контраста попадает в один из проблемных вариантов, кроме схемы «чёрное на белом».
Это самая обидная ошибка в аудите: skip-ссылка существует ровно для того, чтобы помочь человеку, который не видит экран целиком, — и именно у него она не работает.
Нарушено: WCAG 1.4.3 «Контраст (минимум)» (AA), фактически — 2.4.1 «Обход блоков» (A), так как ссылка перестаёт быть различимой.
Рекомендация. Заменить color: white на var(--ui-text-inverted) — эта переменная определена во всех 14 схемах именно как «цвет текста поверх инвертированного фона» и всегда контрастна к --ui-primary. Заодно уйдёт нарушение внутреннего правила проекта «цвета — только через CSS-переменные». Правку нужно внести в двух местах: app/app.vue (блок <style>) и app/assets/css/main.css — определение продублировано.
3. Границы не достигают минимума ни в одной обычной теме
--ui-border отвечает за рамки полей ввода, контуры карточек, разделители таблиц. WCAG 1.4.11 «Контраст нетекстовых элементов» требует для границ элементов интерфейса не менее 3:1 к фону.
| Тема оформления | Значение, :1 |
|---|---|
| Бежевая (тёмная) | 1,92 |
| Голубая (тёмная) | 1,47 |
| Бежевая (сепия) | 1,45 |
| Серая (светлая) | 1,42 |
| Серая (тёмная) | 1,37 |
| Зелёная (тёмная) | 1,26 |
| Голубая (светлая) | 1,24 |
| Светлая базовая | 1,23 |
| Тёмная базовая | 1,22 |
| Зелёная (светлая) | 1,16 |
Источник: Норма WCAG 1.4.11 (AA) — 3,0:1. Высококонтрастные схемы дают 7,18–21,00:1 и в график не включены
Ни одна из десяти обычных схем не проходит. Практически это значит, что границу поля ввода не видно — слабовидящий человек не понимает, где начинается и где заканчивается поле, особенно в форме записи в библиотеку или в фильтрах каталога.
Рекомендация. Ввести отдельную переменную для границ интерактивных элементов — например --ui-border-interactive — и задать её на 2–3 ступени темнее в светлых темах и на 2–3 ступени светлее в тёмных, добившись ≥3:1. Декоративные разделители (--ui-border) при этом можно оставить как есть: к чисто декоративным линиям требование 1.4.11 не применяется. Разделение важно — если просто «затемнить всё», интерфейс станет визуально грубым, а это ухудшит впечатление зрячих пользователей.
Важно
4. Приглушённый текст не проходит AA в трёх темах
--ui-text-dimmed используется для второстепенных подписей — дат, счётчиков, вспомогательных пояснений.
| Тема | Контраст | Норма 4,5:1 |
|---|---|---|
| Светлая базовая | 2,56:1 | не пройдена |
| Тёмная базовая | 3,75:1 | не пройдена |
| Серая (тёмная) | 4,04:1 | не пройдена |
| Остальные 11 схем | 5,02–21,00:1 | пройдена |
Формально WCAG допускает 3:1 для крупного текста (от 24px или 19px полужирного), но подписи этим кеглем не набирают.
Рекомендация. Сдвинуть --ui-text-dimmed в светлой теме с neutral-400 на neutral-500 (даст 4,76:1), в тёмной — с neutral-500 на neutral-400 (6,96:1). Визуально разница едва заметна, а норма будет выполнена.
5. Акцентный цвет теряется на приподнятых подложках
--ui-primary проверялся на контраст к основному фону — там всё хорошо (4,72–9,83:1). Но ссылки и активные элементы часто лежат внутри карточек, то есть на --ui-bg-elevated, и там запас съедается:
| Тема | Акцент на приподнятой подложке |
|---|---|
| Голубая (светлая) | 3,84:1 |
| Бежевая (сепия) | 3,94:1 |
| Серая (светлая) | 4,13:1 |
| Серая (тёмная) | 4,17:1 |
| Зелёная (светлая) | 4,20:1 |
| Зелёная (тёмная) | 4,23:1 |
Шесть схем из десяти не дотягивают до 4,5:1. Отдельно тот же эффект есть у --ui-text-muted: в светлой базовой теме он даёт 4,76:1 на фоне страницы, но 4,34:1 на приподнятой подложке.
Наглядный случай — боковая навигация: .nav-link окрашен в --ui-text-muted, а при наведении получает фон --ui-bg-muted. То есть при наведении контраст падает ниже нормы — ровно в тот момент, когда пользователь целится в ссылку.
Рекомендация. Проверять токены не только к --ui-bg, но и к --ui-bg-muted / --ui-bg-elevated. Простейшее решение — сделать приподнятые подложки на 1 ступень ближе к фону страницы (в светлых темах светлее, в тёмных темнее), тогда запас вернётся без изменения цвета текста.
6. Панель доступности не сообщает, что именно выбрано
Здесь любопытная асимметрия. Панелька «Как читать» над текстом материала (ReadingControls.vue) размечена правильно: role="group", aria-label, aria-pressed на каждой кнопке-переключателе. А в главной панели доступности (AccessibilityPanel.vue) aria-pressed стоит только у группы «Выравнивание текста» — видимо, она добавлялась позже и по свежим правилам.
Остальные пять групп передают состояние только визуально — рамкой, заливкой, галочкой:
| Группа | Состояние доступно скрин-ридеру |
|---|---|
| Цветовая тема | нет |
| Цвет акцента | нет |
| Фон / контрастная схема | нет |
| Размер текста | нет |
| Шрифт | нет |
| Выравнивание текста | да |
Полужирный, Переносы (USwitch) | да |
Смягчает ситуацию то, что каждое переключение озвучивается через announceChange(). Но озвучивается только изменение: человек, открывший панель, не может узнать, какая схема стоит сейчас, — ему придётся перещёлкать всё подряд.
Нарушено: WCAG 4.1.2 «Имя, роль, значение» (A) — состояние не передано программно.
Рекомендация. Добавить :aria-pressed на кнопки всех пяти групп и обернуть каждую в role="group" с aria-labelledby, указывающим на уже существующий заголовок группы. Заголовки сейчас размечены как <p> и с группами не связаны — достаточно проставить им id. Изменение чисто атрибутивное, вид панели не поменяется.
7. Компоненты с жёстко заданными цветами игнорируют выбранную схему
В 18 файлах найдено 76 обращений к палитре Tailwind напрямую (bg-blue-50, text-gray-500) вместо CSS-переменных — это нарушает и правило проекта, и доступность. Больше всего в двух публичных компонентах: content/Alert.vue (24) и DocumentList.vue (22).
Такие компоненты умеют различать только светлую и тёмную тему и не знают ни о восьми фоновых схемах, ни о четырёх высококонтрастных. Поскольку высокий контраст принудительно включает тёмный режим, в дело идут dark:-варианты — и получается вот что:
| Схема | Иконка ::alert на плашке | Рамка ::alert | Карточка документа к фону |
|---|---|---|---|
| Чёрное на белом | 1,08–1,42:1 | 7,09–8,72:1 | 17,74:1 |
| Белое на чёрном | 7,26–11,79:1 | 2,41–2,96:1 | 1,18:1 |
| Жёлтое на синем | 4,72–7,89:1 | 1,25–1,53:1 | 1,63:1 |
| Терминал | 7,26–11,79:1 | 2,41–2,96:1 | 1,18:1 |
Текст внутри блоков читается — он использует переменные и остаётся контрастным. А вот:
- иконка типа сообщения (информация / готово / внимание / ошибка) в схеме «чёрное на белом» практически невидима при всех четырёх типах — то есть теряется сам сигнал «это предупреждение»;
- рамка плашки не различима в трёх схемах из четырёх;
- карточка документа сливается с фоном страницы в трёх схемах из четырёх — список документов превращается в сплошное полотно без границ между файлами.
Нарушено: WCAG 1.4.11 «Контраст нетекстовых элементов» (AA), 1.4.1 «Использование цвета» (A) для иконок-индикаторов.
Рекомендация. Перевести Alert.vue и DocumentList.vue на переменные: подложку — на --ui-bg-elevated, рамку — на --ui-border, а тип сообщения различать переменными состояний --ui-success / --ui-warning / --ui-error / --ui-info. Дополнительно у иконки в Alert.vue стоит aria-hidden="true" при том, что заголовок («Внимание», «Ошибка») выводится текстом — это верно и менять не нужно.
8. Две CSS-переменные используются, но нигде не определены
--ui-primary-hover— используется вProseA.vue(hover:text-[var(--ui-primary-hover)]);--ui-primary-50— используется вSectionNav.vue(с запасным значениемrgba(0, 220, 130, 0.05)).
Ни одна из них не объявлена ни в main.css, ни в app.config.ts. Для --ui-primary-hover запасного значения нет, поэтому свойство становится недействительным на этапе вычисления, и цвет наследуется от родителя: при наведении ссылка в тексте теряет акцентный цвет и становится обычным текстом. Ссылка остаётся подчёркнутой, так что катастрофы нет, но обратная связь при наведении работает наоборот — цвет не усиливается, а пропадает.
У --ui-primary-50 запасное значение есть, но оно жёстко зелёное: читатель, выбравший синий или красный акцент, получит зелёную подсветку активного пункта навигации.
Рекомендация. Либо объявить обе переменные в main.css для каждой темы, либо заменить их на существующие токены (--ui-primary и color-mix() от него).
9. Режим повышенного контраста операционной системы не срабатывает
В конце main.css есть блок:
@media (prefers-contrast: more) {
:root {
--ui-primary: CanvasText;
--ui-text: CanvasText;
--ui-bg: Canvas;
--ui-border: CanvasText;
}
}
Задумка верная — подхватывать системный режим Windows High Contrast. Но селектор :root имеет специфичность (0,1,0), а классы акцента html.a11y-accent-green — (0,1,1) и объявлены выше. Правило для --ui-primary проигрывает и не применяется никогда. Кроме того, блок переопределяет только четыре переменные из двадцати: --ui-bg-muted, --ui-bg-elevated, --ui-text-muted и остальные остаются от обычной темы, так что цельной картины всё равно не получится.
Рекомендация. Либо поднять специфичность до html:root и дополнить набор переменных до полного, либо убрать блок совсем: у сайта есть четыре собственные высококонтрастные схемы, которые решают ту же задачу лучше. Полумера хуже обоих вариантов — она создаёт ложное ощущение поддержки.
Стоит поправить
10. Плавная прокрутка не спрашивает разрешения
В CSS prefers-reduced-motion обработан правильно. Но в трёх местах прокрутка вызывается из JavaScript напрямую, в обход настройки:
| Файл | Строка |
|---|---|
app/app.vue | 245 — переход по skip-ссылке |
app/pages/catalog/index.vue | 737 |
app/pages/about/departments.vue | 218 |
scrollIntoView({ behavior: 'smooth' }) игнорирует медиазапрос. Для человека с вестибулярными нарушениями резкое плавное «уползание» страницы вызывает физическое недомогание.
Рекомендация. Читать настройку через useMediaQuery('(prefers-reduced-motion: reduce)') (VueUse уже подключён) и подставлять behavior: 'auto'. Особенно важно в app.vue: skip-ссылкой пользуются именно те, кому плавность вредна.
11. Таблицы: подписи и привязка заголовков
Из 27 таблиц в проекте:
- 19 без
<caption>— скрин-ридер объявляет «таблица, 6 столбцов, 40 строк» без указания, что это за таблица; - 14 содержат
<th>без атрибутаscope— при переходе по ячейкам не зачитывается заголовок столбца или строки.
Из публичных страниц это затрагивает карточку книги в каталоге (catalog/[id].vue) и раздел «Мои книги» в личном кабинете (dashboard/books.vue, три таблицы). Остальное — рабочее место библиотекаря и админка, но их тоже используют сотрудники с нарушениями зрения.
Хороший образец уже есть в проекте: ScheduleDialog.vue — там честная <table> с <th scope="row"> и комментарием, зачем так сделано.
Рекомендация. Проставить scope="col" / scope="row" и добавить <caption>. Если подпись не нужна визуально — <caption class="sr-only">. Отдельно стоит добавить <caption>-слот в ProseTable.vue, чтобы таблицы в текстах статей получали подписи из markdown.
12. Автозаполнение полей
Десять полей типа email, tel и password не имеют атрибута autocomplete:
| Файл | Поля |
|---|---|
components/UserForm.vue | |
components/ContentEditor.vue | |
components/librarian/ReaderCardForm.vue | tel, email |
pages/account.vue | |
pages/admin/users/[id].vue | tel |
pages/librarian/readers/[id].vue | tel, email |
pages/librarian/readers/new.vue | tel, email |
WCAG 1.3.5 «Определение назначения ввода» (AA) требует этот атрибут для полей, собирающих данные о самом пользователе. Практическая польза прямая: браузер и менеджер паролей подставят значение, и человеку с тремором или слабым зрением не придётся набирать адрес вручную.
Рекомендация. Проставить autocomplete="email", autocomplete="tel", autocomplete="current-password" / "new-password". В полях библиотекаря, куда вводят данные читателя, а не свои, автозаполнение нужно наоборот отключить: autocomplete="off" — иначе браузер начнёт подставлять данные сотрудника в карточку читателя.
13. Мелкие, но заметные недочёты
Четыре иконочные кнопки без доступного имени — VeteranEditor.vue (строки 555, 562, 569) и admin/users/index.vue (286). Скрин-ридер прочитает «кнопка» без пояснения. Лечится атрибутом aria-label.
Одно изображение без alt — ProjectNewsItem.vue:152, превью внешней новости. Если картинка декоративная, нужен alt="", если несёт смысл — осмысленное описание.
Расплывчатый текст ссылок — «Перейти» в NotificationBell.vue:193 и «Подробнее» в content/Card.vue:117. Пользователи скрин-ридеров часто просматривают страницу списком ссылок, и десяток одинаковых «Подробнее» бесполезен. Достаточно скрытого уточнения: Подробнее<span class="sr-only"> о проекте «Название»</span>.
Нет ориентира поиска — role="search" не используется нигде. Поиск по сайту и поиск по каталогу стоит пометить: это стандартный ориентир, по которому переходят одной командой скрин-ридера.
Шесть страниц без заголовка вкладки — весь раздел pages/librarian/* (читатели, выдачи) не задаёт useHead, поэтому во вкладке остаётся общий заголовок сайта. Скрин-ридер объявляет заголовок при каждом переходе — без него библиотекарь не понимает, куда попал. WCAG 2.4.2 (A).
Заголовок h4 в выпадающем списке подсказок каталога (catalog/index.vue:936) создаёт разрыв в структуре: h1 → h4. Внутри всплывающего списка заголовок вообще лишний — группа уже связана через aria-labelledby, достаточно заменить <h4> на <div> с тем же id.
Ссылки на документы теряют подчёркивание. В ProseA.vue для файлов .pdf, .doc, .docx задано text-decoration: none — они отличаются от текста только фоном-плашкой и иконкой. Иконка спасает положение, но подчёркивание здесь лучше вернуть.
Шапка уезжает вверх при прокрутке на мобильных. Если фокус клавиатуры находится на элементе шапки, а страница прокручивается, элемент может оказаться за пределами экрана — при этом он остаётся в порядке обхода. WCAG 2.2 добавил на этот случай критерий 2.4.11 «Фокус не перекрыт» (AA). Достаточно не прятать шапку, пока фокус внутри неё (:focus-within).
Мелкий кегль в боковой навигации. .nav-link — 0,8125rem, а вложенные уровни 0,75rem, то есть около 13,5px при базовых 18px. Размер задан в rem и масштабируется настройками, так что формально нарушения нет, но для сайта библиотеки для слепых и слабовидящих навигация мельче основного текста почти вдвое — спорное решение. Высота строки при этом выходит около 25px при рекомендованных 44px для сенсорных целей (минимум по WCAG 2.5.8 — 24px, то есть на самой границе).
Отдельно: типографика для зрячих
Стандарты доступности часто воспринимают как налог на красоту. Здесь это не так — и текущие настройки сайта хороший тому пример.
Что работает на обе аудитории сразу. База 18px вместо 16px — это просто современная типографика, так набирают The Guardian и Medium. Ограничение строки 75 символами — классическое книжное правило, комфортное всем. Межстрочный 1,6 — то, что рекомендуют и WCAG, и любой учебник вёрстки. PT Sans и PT Serif — качественные шрифты с полной кириллицей, разработанные как раз под русскую типографику.
Что заслуживает похвалы отдельно. Выключка по формату с автоматическими переносами (hyphenate-limit-chars: 6 3 3) включена по умолчанию — текст выглядит как книжная полоса, а не как «рваный» веб. При этом читатель может отключить и то, и другое. Подписи, сноски и таблицы из-под выключки выведены — это уже тонкая работа, её делают немногие. Красная строка в научных статьях раздела НИД — тоже верное решение: там она уместна, а в новостях была бы лишней.
Что можно улучшить, не навредив доступности:
- Висячая пунктуация и неразрывные пробелы. В русском тексте предлоги и союзы не должны оставаться в конце строки. Сейчас этого нет, и при узкой колонке появляются висячие «в», «и», «на». Решается либо типографом на этапе сборки контента, либо
hanging-punctuation: first last(поддержка пока частичная). - Кавычки и тире. Стоит проверить контент на «лапки» вместо «ёлочек» и на дефис вместо тире — на сайте библиотеки это заметят.
font-variant-numeric: tabular-numsдля таблиц. Цифры в колонках выровняются по разрядам. Мелочь, которая сильно поднимает опрятность таблиц с выдачами и статистикой.text-wrap: balanceдля заголовков. Одна строка CSS избавляет от заголовков, где на второй строке висит одно слово. Поддержка в современных браузерах уже хорошая, деградация безопасная.- Ограничение 75ch и приподнятые подложки. Правило
max-width: 75chприменяется кpиliвнутри.prose, но не к заголовкам и подписям — из-за этого длинныйh2может оказаться заметно шире текста под ним. Стоит распространить ограничение и на заголовки.
План работ
Разбито по трудоёмкости и эффекту. Оценки — в человеко-часах, приблизительно.
| Этап | Работы | Эффект | Оценка |
|---|---|---|---|
| 1. Срочно | Skip-ссылка: color: white → var(--ui-text-inverted) (2 файла) | Возвращает работоспособность skip-ссылки 8 схемам из 14 | 15 мин |
Снять роли menubar / menu / menuitem в SectionNav.vue | Навигация начинает работать со скрин-ридером | 30 мин | |
| 2. Токены | Ввести --ui-border-interactive ≥ 3:1 во всех темах | Границы полей и карточек становятся видимыми | 3–4 ч |
Поправить --ui-text-dimmed в 3 темах | Второстепенные подписи проходят AA | 30 мин | |
Сблизить --ui-bg-elevated с фоном в 6 темах | Акцент и приглушённый текст проходят AA в карточках | 2–3 ч | |
Объявить --ui-primary-hover и --ui-primary-50 | Ссылки корректно реагируют на наведение | 1 ч | |
| 3. Компоненты | Alert.vue и DocumentList.vue на CSS-переменные | Плашки и карточки работают во всех 14 схемах | 3–4 ч |
aria-pressed + role="group" в панели доступности | Состояние настроек доступно скрин-ридеру | 1–2 ч | |
| Остальные 30 обращений к палитре Tailwind | Соответствие правилу проекта | 2–3 ч | |
| 4. Разметка | scope и <caption> в 19 таблицах | Таблицы становятся навигируемыми | 3–4 ч |
autocomplete в 10 полях | Меньше ручного ввода | 1 ч | |
useHead на 6 страницах библиотекаря | Понятные заголовки вкладок | 1 ч | |
aria-label на 4 кнопки, alt на 1 изображение, 2 ссылки | Устранение точечных пробелов | 1 ч | |
role="search", prefers-reduced-motion в JS-прокрутке | Ориентиры и безопасная анимация | 1–2 ч | |
| 5. Типографика | Неразрывные пробелы, tabular-nums, text-wrap: balance, 75ch для заголовков | Опрятность текста | 3–4 ч |
Суммарно — примерно 25–30 часов. Первый этап занимает менее часа и снимает обе критические проблемы.
Чем проверять результат
Автоматические средства ловят от силы треть проблем, поэтому список смешанный.
Проверка кода и сборки. Плагин eslint-plugin-vuejs-accessibility (ловит отсутствующие alt, кликабельные div, неверные роли — большую часть найденного в разделе «Стоит поправить» он бы отловил сам). axe DevTools в браузере — на каждой ключевой странице во всех 14 схемах.
Ручная проверка — обязательна.
- Только клавиатура. Пройти главную, каталог, форму записи и личный кабинет, не касаясь мыши. Фокус должен быть виден всегда и двигаться в логичном порядке.
- NVDA (бесплатный, самый распространённый среди незрячих в России) — прослушать те же сценарии. Именно здесь всплывёт проблема с
menubar. - Масштаб 200% и 400% в браузере — по WCAG 1.4.10 контент не должен требовать горизонтальной прокрутки при ширине 320px.
- Все 14 схем на одной сложной странице (каталог с открытыми фильтрами) — этот аудит показал, что проблемы прячутся именно в редких сочетаниях.
- Живой пользователь. Никакой чеклист не заменит получаса наблюдения за читателем библиотеки, который ищет книгу с включённым скрин-ридером.
Итог
Сайт СОСБНС уже сейчас доступнее большинства российских библиотечных сайтов: продуманная система из 14 цветовых схем, озвучиваемые настройки, корректные живые области, образцовые компоненты ссылок и графиков.
Найденные проблемы носят системный, но локальный характер: две критические правки занимают менее часа, а работа с цветовыми токенами — основной объём — сосредоточена в одном файле main.css и распространится на весь сайт сразу.
Главный вывод методического свойства: проверять токены нужно во всех сочетаниях тем, а не только в базовой светлой. Почти всё серьёзное, что нашёл аудит, — от нечитаемой skip-ссылки до исчезающих карточек документов — прячется именно в тёмных и высококонтрастных схемах. То есть там, где сидит основной читатель этой библиотеки.
Что исправлено
Раздел добавлен 18 августа 2026, после выполнения плана работ. Все правки
проверены расчётом контраста по фактическим значениям из main.css
(386 проверок, 0 нарушений), ESLint и рендером страниц.
Этап 1 — критическое
Skip-ссылка. Пара --ui-primary / white заменена на --ui-bg-inverted /
--ui-text-inverted — единственную в системе, чья контрастность гарантирована
самим определением токенов. Было от 1,00:1 до 5,10:1 в зависимости от схемы,
стало от 7,18:1 до 21:1, то есть AAA во всех 14. Кольцо фокуса переведено с
--ui-border-accented (около 1,4:1 к фону) на --ui-primary.
Навигация по разделам. Из SectionNav.vue удалены 10 атрибутов:
role="menubar", role="menu", role="menuitem", role="none" и
aria-orientation. Ссылки снова объявляются ссылками, список — списком.
В компоненте оставлен комментарий, объясняющий, почему роли меню там не нужны.
Этап 2 — цветовые токены
- Введён
--ui-border-interactiveво всех 14 схемах, ≥3:1 к фону, приглушённой и приподнятой подложкам. На него переведены 37 нативных полей и списков в 14 файлах и главное поле поиска каталога. Декоративный--ui-borderоставлен мягким — критерий 1.4.11 к разделителям не относится. --ui-text-dimmedпоправлен в 4 схемах,--ui-text-muted— в 3.- Акцент сдвинут на ступень: 700 → 800 в светлых темах, 400 → 300 в тёмных. Проверка вскрыла, что в отчёте выше проблема недооценена: считался только зелёный акцент, а с красным и синим на приподнятой подложке выходило 2,74:1. Теперь худший случай — 4,5:1, а белый текст на кнопке дал 6,85:1 вместо 4,92:1.
--ui-bg-elevatedзатемнён в двух тёмных схемах, где фон сам по себе светлый.- Объявлены
--ui-primary-hoverи--ui-primary-50черезcolor-mix()от акцента, с запасными значениями для браузеров без его поддержки. - Блок
@media (prefers-contrast: more)переписан: специфичность поднята (он не срабатывал), набор переменных доведён с 4 до 17, а действие ограничено темами по умолчанию — явный выбор читателя важнее общесистемной настройки.
Этап 3 — компоненты
Alert.vueиCompactDocumentList.vueпереведены на переменные: цвет подмешивается к--ui-text-highlighted, который контрастен к фону в любой схеме по определению. Доля 55% проверена по всем 14 схемам.- В
Alert.vueзаодно убранrole="alert": это ассертивная живая область, и на статичной врезке она заставляла скрин-ридер прерывать чтение страницы. Теперь<aside>с именем от собственного заголовка. - В панель доступности добавлены
aria-pressedиrole="group"пяти группам — теперь слышно не только смену настройки, но и текущее состояние.
Этап 4 — разметка
- 82 атрибута
scopeи 18 подписей<caption class="sr-only">. Отдельно созданProseTh.vue: он добавляетscope="col"всем таблицам из markdown разом. - 10 полей получили
autocomplete. В формах библиотекаря выставленоoff: туда вводят данные читателя, и подстановка контактов сотрудника только мешала бы. - 6 страниц рабочего места получили заголовок вкладки; у карточки читателя он динамический, с именем.
role="search"на поиске по сайту и по каталогу — раньше этого ориентира не было ни одного.- Плавная прокрутка в трёх местах теперь спрашивает
prefers-reduced-motion. - Точечно: три кнопки в редакторе ветеранов получили имена; у кнопки внутри
ссылки в админке к
aria-hiddenдобавленtabindex="-1"; уточнены ссылки «Перейти» и «Подробнее»; шапка перестала уезжать, пока внутри неё фокус (WCAG 2.4.11); ссылкам на документы возвращено подчёркивание.
Этап 5 — типографика
Ограничение 75ch распространено на заголовки, добавлены text-wrap: balance
для заголовков и pretty для абзацев, tabular-nums для таблиц.
Неразрывные пробелы делает трансформер app/transformers/typography.ts. Он
работает по дереву разобранного контента и правит только текстовые узлы, не
заходя в code и pre. Связываются лишь предлоги и союзы в 1–2 буквы:
трёхбуквенные намеренно исключены, потому что длинные неразрывные цепочки при
ширине 320 px и кегле 150% грозят горизонтальной прокруткой — а это уже
нарушение 1.4.10. По факту 8 142 склейки из двух слов, максимум — 4 слова
длиной 21 символ.
Что аудит определил неверно
Честности ради:
- Изображение без
altвProjectNewsItem.vue— ложная тревога.alt=""на месте, обёртка помеченаaria-hidden, а у ссылки есть полное описание. Сканер поймал тег<img>внутри текста комментария. - Кнопка без имени в
admin/users— тоже ложная: имя даёт обрамляющая ссылка. Зато там нашлось другое:aria-hiddenна фокусируемой кнопке. DocumentList.vueв отчёте назван публичным компонентом, но он нигде не подключён — на страницах работаетCompactDocumentList.vue. Исправлены оба.- Тёмные темы оказались хуже, чем сказано выше (см. этап 2).
Что осталось за рамками
- Трансформеры Nuxt Content не подхватываются автоматически — их нужно
перечислять в
nuxt.config.ts. Списка не было вовсе, поэтому существующийapp/transformers/reading-time.tsникогда не выполнялся. Добавлен только типографский: включение расчёта времени чтения изменит вид страниц, и это отдельное решение. - Кавычки-«ёлочки» и тире в текстах — правка контента, а не кода.
- 110 ошибок типов и 30 ошибок ESLint в проекте существовали до этих работ и к доступности отношения не имеют; ни одной новой не добавлено.
- Ручная проверка на NVDA, при масштабе 200–400% и с живым читателем по-прежнему обязательна — расчёт контраста её не заменяет.