Классические инструменты автоматизированного тестирования доступности (Accessibility / a11y), такие как axe-core, pa11y или Google Lighthouse, базируются на статическом анализе DOM-дерева и вычисленных стилей CSS. Согласно исследованиям W3C и Deque Systems, алгоритмические правила способны выявить лишь от 30% до 40% нарушений спецификаций WCAG (Web Content Accessibility Guidelines).
Ключевые ограничения классических анализаторов:
axe-core считывают статическое CSS-свойство background-color. Если текст расположен поверх линейного градиента, полупрозрачной подложки (backdrop-filter: blur), фонового растрового изображения или видеопотока, сканеры не могут вычислить локальный контраст и либо выдают статус incomplete (требуется ручная проверка), либо генерируют ложноположительные отчеты.alt у тега <img>. Строки вида alt="image", alt="banner123" или alt="график" формально проходят проверку линтера, но являются грубым нарушением критерия WCAG 1.1.1 (Non-text Content), оставляя пользователей скринридеров без информации.<canvas> или сложных деревьях <svg> (библиотеки Chart.js, Recharts, Three.js). Для вспомогательных технологий (Assistive Technologies) эти графики представляют собой недоступные черные ящики.В 2026 году архитектура проверки доступности переходит на гибридную мультимодальную триангуляцию: объединение Accessibility Tree браузера (CDP AXTree), DOM-манифеста и растрового скриншота высокого разрешения под управлением Vision LLM.
Триангуляция доступности сопоставляет три параллельных проекции интерфейса:
[ Chrome DevTools Protocol (CDP) / Playwright ]
|-- 1. Accessibility Tree Snapshot (Roles, Names, Values, States)
|-- 2. Cleaned Semantic DOM (HTML5 elements, ARIA attributes)
`-- 3. High-Resolution Viewport Render (Visual Context Canvas)
\ | /
\ | /
v v v
+---------------------------------------+
| Hybrid A11y Evaluation Engine |
+---------------------------------------+
|
[ Multimodal Reasoning (Vision LLM) ]
|
+--------------------------+--------------------------+
| |
v v
[ Local Colorimetric Contrast ] [ Semantic Content Generation ]
|-- WCAG 2.2 AA/AAA Verification |-- Meaningful 'alt' descriptions
|-- Gradient & Image Contrast Detection |-- Rich 'aria-label' for Dataviz
`-- Focus Visibility & Target Size `-- Structured Table FallbacksФормула вычисления коэффициента контрастности по стандарту WCAG:
Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
где:
L1 — относительная яркость более светлого цвета [0..1]
L2 — относительная яркость более темного цвета [0..1]
Относительная яркость L для sRGB-компонентов (R, G, B):
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin
где для каждого канала C in {R, G, B}:
если (C / 255) <= 0.04045 -> C_lin = (C / 255) / 12.92
иначе -> C_lin = (( (C / 255) + 0.055 ) / 1.055) ^ 2.4aria-label и скрытых таблиц данных (sr-only) для скринридеров.alt, aria-label и title с автоматической заменой на контекстно-осмысленные описания.Ниже представлена production-ready реализация гибридного a11y-аудитора на TypeScript, извлекающего снимок Accessibility Tree через Playwright и передающего его вместе со скриншотом в Vision LLM для глубокого анализа WCAG 2.2.
import { chromium, type Browser, type Page } from 'playwright';
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// 1. Zod-схема отчета об аудите доступности
export const A11yViolationSchema = z.object({
ruleId: z.string().describe('Идентификатор критерия WCAG (например, WCAG_1_4_3_CONTRAST)'),
wcagLevel: z.enum(['A', 'AA', 'AAA']),
impact: z.enum(['CRITICAL', 'SERIOUS', 'MODERATE', 'MINOR']),
elementSelector: z.string().describe('CSS селектор проблемного элемента'),
issueSummary: z.string().describe('Суть нарушения доступности с учетом визуального контекста'),
computedContrastRatio: z.number().optional().describe('Фактический локальный контраст текста и фона'),
requiredContrastRatio: z.number().optional().describe('Минимально допустимый контраст по WCAG 2.2'),
recommendedAriaPatch: z.object({
attribute: z.enum(['alt', 'aria-label', 'aria-describedby', 'role']),
suggestedValue: z.string(),
}).optional().describe('Рекомендуемый патч семантических атрибутов'),
});
export const A11yAuditResultSchema = z.object({
pageUrl: z.string(),
wcagComplianceScore: z.number().min(0).max(100),
isCompliantAA: z.boolean(),
violations: z.array(A11yViolationSchema),
accessibleDataVizSummaries: z.array(
z.object({
targetSelector: z.string(),
chartType: z.string(),
generatedSrOnlyTableHtml: z.string().describe('Семантическая HTML-таблица для скрытого озвучивания скринридерами'),
conciseAriaLabel: z.string().describe('Лаконичное резюме тренда для aria-label'),
})
),
});
export type A11yAuditResult = z.infer<typeof A11yAuditResultSchema>;
/**
* Захват триангуляционных данных: скриншот + a11y tree + очищенный DOM
*/
export async function captureA11yContext(url: string): Promise<{
screenshot: Buffer;
accessibilityTree: any;
interactiveElements: string;
}> {
const browser: Browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
deviceScaleFactor: 2,
});
const page: Page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle' });
// 1. Получение снимка дерева доступности Chromium
const accessibilitySnapshot = await page.accessibility.snapshot({
interestingOnly: false, // Захват полного дерева, включая скрытые узлы
});
// 2. Извлечение семантического среза интерактивных элементов и медиа
const interactiveElements = await page.evaluate(() => {
const targets = document.querySelectorAll('button, a, input, [role], svg, canvas, img');
return Array.from(targets).map((el) => {
return {
tag: el.tagName.toLowerCase(),
role: el.getAttribute('role') || el.computedRole,
name: el.getAttribute('aria-label') || (el as HTMLElement).innerText || el.getAttribute('alt') || '',
selector: el.id ? `#${el.id}` : el.className ? `.${el.className.split(' ')[0]}` : el.tagName.toLowerCase(),
hasImageOrCanvas: el.tagName === 'CANVAS' || el.tagName === 'SVG' || el.tagName === 'IMG',
};
});
});
// 3. Захват растрового скриншота вьюпорта
const screenshot = await page.screenshot({ fullPage: false, type: 'png' });
return {
screenshot,
accessibilityTree: accessibilitySnapshot,
interactiveElements: JSON.stringify(interactiveElements, null, 2),
};
} finally {
await context.close();
await browser.close();
}
}
/**
* Мультимодальный аудит доступности через Vision LLM
*/
export async function auditAccessibilityWithVision(params: {
url: string;
screenshot: Buffer;
accessibilityTree: any;
interactiveElements: string;
}): Promise<A11yAuditResult> {
const base64Screenshot = params.screenshot.toString('base64');
const systemPrompt = `
Вы — ведущий мировой аудитор доступности цифровых интерфейсов и эксперт по WCAG 2.2 (уровни A, AA, AAA).
Вам предоставлены:
1. Физический растровый скриншот интерфейса.
2. Снимок Accessibility Tree (роли, имена, состояния скринридера).
3. Список интерактивных узлов страницы.
Ваши задачи:
1. Оценить контрастность текста на сложных фонах (градиенты, фото, полупрозрачные карточки).
- Минимальный порог WCAG 2.2 AA для обычного текста: 4.5:1.
- Минимальный порог для крупного текста (>=18pt или >=14pt bold): 3.0:1.
2. Проанализировать графики (Canvas/SVG): если график не имеет понятного текстового описания данных,
сгенерируйте лаконичный 'conciseAriaLabel' с описанием главного тренда и доступную HTML-таблицу 'generatedSrOnlyTableHtml'.
3. Выявить бессмысленные заглушки в alt-текстах (например, 'banner', 'photo', 'icon') и предложить точные описания.
4. Проверить размер сенсорных мишеней (Target Size Minimum 24x24px).
5. Результат верните строго в формате переданной Zod-схемы.
`.trim();
const userContent = `
URL страницы: ${params.url}
Срез интерактивных DOM-узлов:
${params.interactiveElements}
Фрагмент дерева доступности (a11y tree snapshot):
\`\`\`json
${JSON.stringify(params.accessibilityTree, null, 2).slice(0, 15000)}
\`\`\`
Сопоставьте визуальный скриншот с деревом доступности и сформируйте исчерпывающий отчет.
`.trim();
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: A11yAuditResultSchema,
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: [
{ type: 'text', text: userContent },
{ type: 'image', image: `data:image/png;base64,${base64Screenshot}` },
],
},
],
});
return object;
}
/**
* Пример интеграции в CI/CD пайплайн
*/
export async function runA11yCiCheck(targetUrl: string): Promise<boolean> {
console.log(`🔍 Запуск гибридного a11y-аудита для: ${targetUrl}`);
const contextData = await captureA11yContext(targetUrl);
const auditReport = await auditAccessibilityWithVision({
url: targetUrl,
screenshot: contextData.screenshot,
accessibilityTree: contextData.accessibilityTree,
interactiveElements: contextData.interactiveElements,
});
console.log(`📊 Оценка соответствия WCAG 2.2: ${auditReport.wcagComplianceScore}%`);
console.log(`Найдено нарушений: ${auditReport.violations.length}`);
for (const violation of auditReport.violations) {
console.error(
`[${violation.impact}] ${violation.ruleId} на '${violation.elementSelector}': ${violation.issueSummary}`
);
if (violation.recommendedAriaPatch) {
console.log(
` 💡 Патч: ${violation.recommendedAriaPatch.attribute}="${violation.recommendedAriaPatch.suggestedValue}"`
);
}
}
// Блокировка CI, если нарушены критические требования WCAG 2.2 AA
const hasCriticalViolations = auditReport.violations.some(
(v) => v.impact === 'CRITICAL' && v.wcagLevel === 'AA'
);
return !hasCriticalViolations && auditReport.isCompliantAA;
}Частая ошибка при автогенерации доступности — создание многословных описаний для абстрактных фоновых фигур, иконок-разделителей или фоновых градиентов (alt="фиолетовый размытый круг"). Скринридеры обязаны игнорировать декоративные элементы. Для них следует устанавливать пустое значение alt="" или атрибут aria-hidden="true", чтобы не перегружать аудио-фокус пользователя.
Инструменты вроде axe-core анализируют только объявленный CSS color: #ffffff и background-color: #000000. Если над фоном применен полупрозрачный оверлей (opacity: 0.7), фильтр размытия (backdrop-filter) или динамическое видео, статический расчет показывает контраст 21:1, в то время как в реальности под белым текстом проплывает светлый кадр видео, снижая контрастность до 1.5:1. Только анализ фактической растровой матрицы через Vision API дает достоверную оценку.
Попытка поместить подробный анализ 50 точек временного ряда прямо в атрибут aria-label тега <svg>. Скринридер начинает непрерывно зачитывать гигантский текстовый блок без возможности перемотки или навигации по ячейкам. Стандарт требует: краткий вывод тренда в aria-label (например: "Рост выручки на 14% во 2 квартале"), а полные числовые ряды — в скрытой таблице (<table className="sr-only">), связанной через aria-describedby.
Удаление обводки фокуса правилом outline: none без предоставления альтернативного визуального индикатора нарушает критерий WCAG 2.2 (2.4.7 Focus Visible и 2.4.13 Focus Appearance). Мультимодальный аудит должен эмулировать нажатие клавиши Tab через Playwright и подтверждать визуальное наличие контрастной рамки вокруг активного элемента.
Playwright запускает Chromium и фиксирует вьюпорт и эмуляцию медиа-параметров (prefers-contrast)
Одновременный сбор физического растра страницы и иерархического дерева Chromium Accessibility Tree (CDP)
Vision LLM измеряет локальные градиентные контрасты подстрок и вычисляет относительную яркость
Автоматическая генерация текстовых эквивалентов для Canvas/SVG и коррекция дефектных alt-атрибутов
Формирование отчета с проверкой соответствия критериям WCAG 2.2 AA/AAA и отправка в CI/CD