Графики в веб-приложениях часто ориентированы исключительно на визуальное восприятие. Однако полноценный интерфейс должен поддерживать управление не только мышью или сенсорным экраном, но и клавиатурой. Для пользователей со вспомогательными технологиями, ограниченной моторикой или при навигации без мыши поддержка клавиатуры становится обязательным требованием.
По умолчанию Chart.js не предоставляет полноценную систему
клавиатурной навигации. Canvas-элемент не содержит интерактивных
HTML-узлов для отдельных точек, столбцов или сегментов диаграммы. Вся
графика рендерится внутри одного <canvas>, поэтому
разработчику необходимо самостоятельно реализовывать:
Canvas не является семантическим элементом интерфейса. Браузер воспринимает его как единый графический контейнер.
Пример:
<canvas id="chart"></canvas>
С точки зрения accessibility внутри canvas отсутствуют:
Даже если диаграмма визуально содержит десятки столбцов или точек, клавиатура не может переключаться между ними автоматически.
Поэтому поддержка клавиатурной навигации строится вокруг нескольких механизмов:
Поддержку клавиатуры удобно разделять на несколько уровней.
Позволяет:
Добавляет:
Включает:
Canvas не получает фокус автоматически. Для поддержки клавиатуры
необходимо добавить tabindex.
<canvas id="salesChart" tabindex="0"></canvas>
Теперь элемент можно выбрать клавишей Tab.
Без визуального отображения пользователь не поймёт, что график активен.
canvas:focus {
outline: 3px solid #2563eb;
outline-offset: 4px;
}
Такой подход особенно важен при работе с accessibility-аудитом.
<canvas id="salesChart" tabindex="0"></canvas>
const ctx = document.getElementById('salesChart');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Январь', 'Февраль', 'Март', 'Апрель'],
datasets: [{
label: 'Продажи',
data: [12, 19, 8, 15],
backgroundColor: '#3b82f6'
}]
},
options: {
responsive: true
}
});
На этом этапе график уже можно выбрать клавишей Tab, но навигация между столбцами ещё отсутствует.
Для перемещения по данным требуется состояние текущего выбранного элемента.
let activeIndex = 0;
Добавление обработчика клавиатуры:
ctx.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
activeIndex++;
}
if (event.key === 'ArrowLeft') {
activeIndex--;
}
if (activeIndex < 0) {
activeIndex = 0;
}
if (activeIndex >= chart.data.labels.length) {
activeIndex = chart.data.labels.length - 1;
}
updateActiveElement();
});
Chart.js позволяет программно активировать элементы.
function updateActiveElement() {
chart.setActiveElements([
{
datasetIndex: 0,
index: activeIndex
}
]);
chart.tooltip.setActiveElements([
{
datasetIndex: 0,
index: activeIndex
}
]);
chart.update();
}
После этого стрелки клавиатуры начинают переключать столбцы диаграммы.
Метод:
chart.setActiveElements()
эмулирует наведение мыши на элемент.
Это позволяет:
Часто диаграмма используется как интерактивный компонент.
Например:
Поддержка Enter:
ctx.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
const label = chart.data.labels[activeIndex];
const value = chart.data.datasets[0].data[activeIndex];
console.log(label, value);
}
});
Поддержка Space:
if (event.key === ' ') {
event.preventDefault();
}
preventDefault() предотвращает прокрутку страницы
пробелом.
Screen reader должен получать информацию о выбранном элементе.
Создание live region:
<div id="chart-announcer" aria-live="polite"></div>
Обновление текста:
function announceValue() {
const label = chart.data.labels[activeIndex];
const value = chart.data.datasets[0].data[activeIndex];
const announcer = document.getElementById('chart-announcer');
announcer.textContent =
`${label}: ${value}`;
}
Интеграция:
function updateActiveElement() {
chart.setActiveElements([
{
datasetIndex: 0,
index: activeIndex
}
]);
chart.update();
announceValue();
}
ARIA-область не обязана отображаться на экране.
#chart-announcer {
position: absolute;
left: -9999px;
}
Такой элемент остаётся доступным для screen reader.
Canvas поддерживает атрибуты доступности.
<canvas
id="salesChart"
tabindex="0"
role="img"
aria-label="График продаж по месяцам">
</canvas>
Иногда требуется более подробное описание.
<p id="chartDescription">
Диаграмма показывает продажи за первые четыре месяца года.
</p>
<canvas
id="salesChart"
tabindex="0"
aria-describedby="chartDescription">
</canvas>
Если график содержит несколько наборов данных, навигация усложняется.
Пример:
datasets: [
{
label: '2024',
data: [12, 19, 8, 15]
},
{
label: '2025',
data: [16, 22, 11, 18]
}
]
Требуется хранить:
let activeDataset = 0;
let activeIndex = 0;
Например:
ctx.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowRight':
activeIndex++;
break;
case 'ArrowLeft':
activeIndex--;
break;
case 'ArrowUp':
activeDataset--;
break;
case 'ArrowDown':
activeDataset++;
break;
}
normalizeIndexes();
updateActiveElement();
});
function normalizeIndexes() {
const datasetsCount = chart.data.datasets.length;
const itemsCount = chart.data.labels.length;
if (activeDataset < 0) {
activeDataset = 0;
}
if (activeDataset >= datasetsCount) {
activeDataset = datasetsCount - 1;
}
if (activeIndex < 0) {
activeIndex = 0;
}
if (activeIndex >= itemsCount) {
activeIndex = itemsCount - 1;
}
}
function updateActiveElement() {
chart.setActiveElements([
{
datasetIndex: activeDataset,
index: activeIndex
}
]);
chart.tooltip.setActiveElements([
{
datasetIndex: activeDataset,
index: activeIndex
}
]);
chart.update();
}
Линейные графики требуют особенно аккуратной реализации.
Причины:
Пример:
type: 'line'
Навигация работает аналогично bar chart, но рекомендуется:
pointRadius;pointHoverRadius;elements: {
point: {
radius: 4,
hoverRadius: 8
}
}
Можно изменять стили динамически.
function highlightPoint(index) {
chart.data.datasets[0].pointRadius =
chart.data.datasets[0].data.map((_, i) => {
return i === index ? 8 : 4;
});
chart.update();
}
Для accessibility рекомендуется предоставлять альтернативное табличное представление.
<table>
<thead>
<tr>
<th>Месяц</th>
<th>Продажи</th>
</tr>
</thead>
<tbody>
<tr>
<td>Январь</td>
<td>12</td>
</tr>
<tr>
<td>Февраль</td>
<td>19</td>
</tr>
</tbody>
</table>
При перемещении по графику можно выделять строку таблицы.
function highlightTableRow(index) {
const rows =
document.querySelectorAll('tbody tr');
rows.forEach(row => {
row.classList.remove('active');
});
rows[index].classList.add('active');
}
tbody tr.active {
background: #dbeafe;
}
Дополнительные клавиши улучшают UX.
switch (event.key) {
case 'Home':
activeIndex = 0;
break;
case 'End':
activeIndex =
chart.data.labels.length - 1;
break;
}
Иногда удобнее переходить от последнего элемента к первому.
if (activeIndex < 0) {
activeIndex =
chart.data.labels.length - 1;
}
if (activeIndex >= chart.data.labels.length) {
activeIndex = 0;
}
Escape может использоваться для:
if (event.key === 'Escape') {
chart.setActiveElements([]);
chart.update();
}
Tooltip играет важную роль в keyboard navigation.
Настройки:
options: {
plugins: {
tooltip: {
enabled: true
}
}
}
chart.tooltip.setActiveElements([
{
datasetIndex: activeDataset,
index: activeIndex
}
]);
Canvas tooltip не всегда читается screen reader.
Поэтому рекомендуется:
Chart.js позволяет отключить встроенный tooltip.
plugins: {
tooltip: {
enabled: false,
external: externalTooltipHandler
}
}
function externalTooltipHandler(context) {
const tooltip = context.tooltip;
const element =
document.getElementById('tooltip');
if (tooltip.opacity === 0) {
element.style.opacity = 0;
return;
}
const point = tooltip.dataPoints[0];
element.innerHTML =
`${point.label}: ${point.raw}`;
element.style.opacity = 1;
}
HTML-элементы значительно лучше взаимодействуют с:
Если график является частью сложного интерфейса, может потребоваться ограничение фокуса.
Например:
Для сложных интерфейсов удобно динамически переключать tabindex.
canvas.tabIndex = 0;
Или:
canvas.tabIndex = -1;
При горизонтальной прокрутке важно следить за областью видимости.
Например:
canvas.scrollIntoView({
block: 'nearest',
inline: 'nearest'
});
На планшетах и мобильных устройствах с внешней клавиатурой события работают аналогично desktop.
Основные события:
keydown
keyup
Keyboard navigation удобно выносить в отдельный Chart.js plugin.
const keyboardNavigationPlugin = {
id: 'keyboardNavigation',
afterInit(chart) {
const canvas = chart.canvas;
canvas.tabIndex = 0;
let activeIndex = 0;
canvas.addEventListener('keydown', (event) => {
if (event.key === 'ArrowRight') {
activeIndex++;
}
if (event.key === 'ArrowLeft') {
activeIndex--;
}
const max =
chart.data.labels.length - 1;
if (activeIndex < 0) {
activeIndex = 0;
}
if (activeIndex > max) {
activeIndex = max;
}
chart.setActiveElements([
{
datasetIndex: 0,
index: activeIndex
}
]);
chart.update();
});
}
};
Регистрация:
Chart.register(keyboardNavigationPlugin);
Плагин позволяет:
Без tabindex canvas недоступен для клавиатуры.
Пользователь не понимает текущий элемент.
Screen reader не получает информацию.
Canvas tooltip часто недоступен для assistive technologies.
Необходимо избегать конфликтов с браузером и screen reader.
Плохой пример:
event.preventDefault();
для всех клавиш подряд.
Активный элемент должен быть хорошо различим.
Минимальный набор:
aria-label;aria-describedby;aria-live.Табличное представление остаётся наиболее доступным форматом.
Клавиши должны работать предсказуемо:
Поддержка клавиатуры напрямую связана с требованиями WCAG.
Особенно важны:
Крупные приложения обычно строят систему из нескольких компонентов:
Отвечает за:
Содержит:
Управляет:
Связывает: