Графики, построенные через <canvas>, практически
недоступны для скринридеров без дополнительного описания. Элемент canvas
не содержит семантической структуры: внутри отсутствуют текстовые узлы,
заголовки, таблицы и подписи, которые могут быть интерпретированы
вспомогательными технологиями.
Библиотека Chart.js рендерит диаграммы именно через canvas, поэтому
необходимо вручную добавлять доступное описание. Основной инструмент для
этого — атрибут aria-label.
Пример базовой разметки:
<canvas
id="salesChart"
aria-label="График продаж по месяцам: январь — 120, февраль — 150, март — 180"
role="img">
</canvas>
В данном случае:
role="img" сообщает скринридеру, что canvas
представляет изображение;aria-label содержит текстовое описание содержимого
графика.Без этих атрибутов скринридер чаще всего сообщает только наличие canvas без пояснений.
title недостаточноАтрибут title не является полноценным механизмом
доступности:
<canvas title="Продажи за квартал"></canvas>
Проблемы такого подхода:
title;aria-label является стандартным и рекомендуемым способом
описания элементов интерфейса для assistive technologies.
Наиболее распространённый вариант:
<canvas
id="trafficChart"
role="img"
aria-label="Линейный график посещаемости сайта с января по июнь">
</canvas>
Инициализация графика:
const ctx = document.getElementById('trafficChart');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн'],
datasets: [{
label: 'Посетители',
data: [1200, 1500, 1700, 1600, 2100, 2500]
}]
}
});
Такой вариант уже значительно лучше полностью немаркированного canvas.
role="img"Canvas сам по себе не считается изображением с точки зрения accessibility API. Атрибут:
role="img"
сообщает скринридеру, что содержимое нужно воспринимать как единый визуальный объект.
Полная корректная конструкция:
<canvas
role="img"
aria-label="Круговая диаграмма распределения расходов">
</canvas>
Без role="img" некоторые средства доступности могут
проигнорировать aria-label.
aria-labelХорошее описание должно:
Плохой пример:
aria-label="График"
Недостатки:
Хороший пример:
aria-label="Столбчатая диаграмма продаж по кварталам: первый квартал — 120 тысяч, второй — 180 тысяч, третий — 210 тысяч, четвёртый — 260 тысяч"
aria-label автоматическиПри динамических данных ручное описание становится неудобным. Обычно текст генерируется программно.
<canvas id="financeChart" role="img"></canvas>
const labels = ['Январь', 'Февраль', 'Март'];
const values = [120, 180, 240];
const description = labels
.map((month, index) => {
return `${month} — ${values[index]}`;
})
.join(', ');
const canvas = document.getElementById('financeChart');
canvas.setAttribute(
'aria-label',
`График прибыли по месяцам: ${description}`
);
new Chart(canvas, {
type: 'bar',
data: {
labels,
datasets: [{
label: 'Прибыль',
data: values
}]
}
});
Результат:
График прибыли по месяцам: Январь — 120, Февраль — 180, Март — 240
aria-labelledbyЕсли описание уже существует на странице, вместо
aria-label лучше использовать связь через
aria-labelledby.
<h2 id="chartTitle">
Доход компании за 2025 год
</h2>
<canvas
id="incomeChart"
role="img"
aria-labelledby="chartTitle">
</canvas>
Преимущества:
aria-describedbyДля длинного пояснения применяется aria-describedby.
<p id="chartDescription">
Диаграмма показывает рост продаж с января по июнь.
Максимальное значение достигнуто в июне.
</p>
<canvas
id="salesChart"
role="img"
aria-describedby="chartDescription">
</canvas>
В отличие от aria-label, здесь допускаются объёмные
пояснения.
aria-label и aria-describedbyЧасто используется комбинированный подход:
<p id="details">
Наибольший рост наблюдается во втором квартале.
</p>
<canvas
id="growthChart"
role="img"
aria-label="Линейный график роста прибыли"
aria-describedby="details">
</canvas>
Скринридер сначала озвучит краткое название, затем подробности.
Чем сложнее визуализация, тем труднее сформировать понятное описание.
Особенно это касается:
Для сложных графиков рекомендуется:
aria-label;Иногда удобнее хранить полный текст отдельно.
<div class="sr-only" id="chartData">
Продажи по регионам:
север — 120,
юг — 140,
запад — 180,
восток — 160.
</div>
<canvas
id="regionChart"
role="img"
aria-describedby="chartData">
</canvas>
CSS для скрытия:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
}
Такой текст невидим визуально, но доступен скринридерам.
Chart.js хранит данные в datasets, что позволяет
автоматически строить accessibility-описания.
function buildAriaLabel(chartData) {
const dataset = chartData.datasets[0];
return chartData.labels
.map((label, index) => {
return `${label}: ${dataset.data[index]}`;
})
.join(', ');
}
const data = {
labels: ['Пн', 'Вт', 'Ср'],
datasets: [{
label: 'Температура',
data: [18, 20, 23]
}]
};
const canvas = document.getElementById('weatherChart');
canvas.setAttribute(
'aria-label',
`Температура по дням: ${buildAriaLabel(data)}`
);
aria-label при изменении данныхChart.js поддерживает динамическое обновление datasets. Важно синхронно обновлять и описание.
chart.data.datasets[0].data = [10, 20, 30];
chart.update();
Проблема: визуальные данные обновились, а текстовое описание осталось старым.
chart.data.datasets[0].data = [10, 20, 30];
canvas.setAttribute(
'aria-label',
'Новые данные графика: 10, 20, 30'
);
chart.update();
В крупных проектах удобнее вынести генерацию accessibility-описаний в отдельную функцию.
function applyChartAccessibility(canvas, chartConfig) {
const dataset = chartConfig.data.datasets[0];
const values = chartConfig.data.labels
.map((label, index) => {
return `${label} — ${dataset.data[index]}`;
})
.join(', ');
const label =
`${chartConfig.type} chart: ${values}`;
canvas.setAttribute('role', 'img');
canvas.setAttribute('aria-label', label);
}
Использование:
applyChartAccessibility(canvas, config);
Скринридеры не анализируют пиксели canvas. Они работают только с DOM-атрибутами.
Поэтому:
Всё важное содержимое должно существовать в DOM в текстовом виде.
HTML допускает альтернативное содержимое:
<canvas id="chart">
График показывает рост прибыли компании за 2025 год.
</canvas>
Этот текст отображается, если canvas не поддерживается браузером.
Однако современные скринридеры чаще ориентируются именно на
ARIA-атрибуты, поэтому fallback не заменяет aria-label.
Легенды Chart.js рендерятся внутри canvas и потому плохо доступны.
Рекомендуемый подход — внешняя HTML-легенда.
<ul>
<li>Синий — доходы</li>
<li>Красный — расходы</li>
</ul>
Вместо:
plugins: {
legend: {
display: true
}
}
HTML-легенды:
Для сложных данных лучшей практикой считается дублирование информации таблицей.
<canvas
id="reportChart"
role="img"
aria-describedby="reportTable">
</canvas>
<table id="reportTable">
<thead>
<tr>
<th>Месяц</th>
<th>Продажи</th>
</tr>
</thead>
<tbody>
<tr>
<td>Январь</td>
<td>120</td>
</tr>
<tr>
<td>Февраль</td>
<td>180</td>
</tr>
</tbody>
</table>
Это наиболее доступный способ представления аналитики.
<canvas id="chart"></canvas>
Canvas становится «немым» для screen reader.
aria-label="Диаграмма"
Описание бесполезно.
aria-label="Очень длинный текст на несколько абзацев..."
Скринридеру неудобно озвучивать большие объёмы текста.
aria-label="Продажи"
aria-describedby="Продажи"
Информация повторяется дважды.
Очень частая проблема в SPA-приложениях.
<h2 id="visitorsTitle">
Посещаемость сайта
</h2>
<p id="visitorsDescription">
График демонстрирует стабильный рост посещаемости
с января по июнь.
</p>
<canvas
id="visitorsChart"
role="img"
aria-labelledby="visitorsTitle"
aria-describedby="visitorsDescription">
</canvas>
const data = {
labels: [
'Янв',
'Фев',
'Мар',
'Апр',
'Май',
'Июн'
],
datasets: [{
label: 'Посетители',
data: [1200, 1400, 1700, 2100, 2500, 3000],
borderColor: 'blue'
}]
};
new Chart(
document.getElementById('visitorsChart'),
{
type: 'line',
data
}
);
Такой подход обеспечивает: