Подписи для скринридеров через aria-label

Графики, построенные через <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;
  • содержимое 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.

HTML

<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>

Скринридер сначала озвучит краткое название, затем подробности.


Доступность сложных диаграмм

Чем сложнее визуализация, тем труднее сформировать понятное описание.

Особенно это касается:

  • stacked charts;
  • radar charts;
  • mixed charts;
  • bubble charts;
  • heatmaps.

Для сложных графиков рекомендуется:

  1. краткое описание в aria-label;
  2. подробная таблица рядом;
  3. текстовая аналитика;
  4. скрытая расшифровка данных.

Добавление скрытого текстового описания

Иногда удобнее хранить полный текст отдельно.

Пример

<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);
}

Такой текст невидим визуально, но доступен скринридерам.


Генерация описаний из datasets

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();

Создание универсального helper

В крупных проектах удобнее вынести генерацию accessibility-описаний в отдельную функцию.

Пример utility-функции

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);

Особенности screen reader navigation

Скринридеры не анализируют пиксели canvas. Они работают только с DOM-атрибутами.

Поэтому:

  • tooltip Chart.js недоступен;
  • hover-состояния не озвучиваются;
  • легенды внутри canvas игнорируются;
  • визуальные анимации бесполезны для accessibility.

Всё важное содержимое должно существовать в DOM в текстовом виде.


Использование fallback-контента внутри canvas

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-легенды:

  • читаются скринридерами;
  • доступны с клавиатуры;
  • стилизуются через CSS;
  • индексируются поисковыми системами.

Связь графика и таблицы

Для сложных данных лучшей практикой считается дублирование информации таблицей.

Пример структуры

<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>

Это наиболее доступный способ представления аналитики.


Частые ошибки

Отсутствие ARIA-атрибутов

<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
    }
);

Такой подход обеспечивает:

  • поддержку screen reader;
  • семантическое описание;
  • отделение визуального слоя от текстового;
  • корректную работу accessibility API;
  • соответствие WCAG-практикам.