SVG-графика по умолчанию ориентирована прежде всего на визуальное отображение. Экранные дикторы, вспомогательные технологии и поисковые системы воспринимают SVG иначе, чем обычный HTML-документ. Без дополнительной семантики интерактивная диаграмма может оказаться полностью недоступной: пользователь услышит лишь сообщение вроде «graphic» или «group».
В D3.js семантика SVG особенно важна, поскольку библиотека активно
создаёт и изменяет DOM-структуру динамически. Правильное использование
role, aria-label, title и
desc позволяет превратить SVG из набора фигур в полноценный
доступный интерфейс.
SVG-элементы не обладают встроенной семаникой уровня HTML. Например:
<button>Сохранить</button>
Браузер и screen reader сразу понимают назначение кнопки.
SVG-элемент:
<circle cx="50" cy="50" r="40"></circle>
не сообщает ничего о своём смысле.
Для решения этой проблемы используются:
rolearia-* атрибуты<title><desc>Эти механизмы помогают:
roleАтрибут role сообщает вспомогательным технологиям, какую
функцию выполняет SVG-элемент.
Пример:
<svg role="img"></svg>
Screen reader начинает воспринимать SVG как изображение.
role| Role | Назначение |
|---|---|
img |
Графическое изображение |
presentation |
Декоративный элемент |
graphics-document |
Полноценный графический документ |
graphics-symbol |
Графический символ |
button |
Интерактивная кнопка |
group |
Группа элементов |
role="img"Наиболее распространённый вариант для диаграмм.
<svg role="img">
Такой SVG начинает восприниматься как единое изображение.
const svg = d3.select("body")
.append("svg")
.attr("width", 600)
.attr("height", 400)
.attr("role", "img");
role="presentation"Применяется для декоративной графики, не несущей смысловой нагрузки.
<svg role="presentation"></svg>
Screen reader проигнорирует элемент.
d3.select("body")
.append("svg")
.attr("role", "presentation");
Подходит для:
role="button"SVG-элемент может выступать интерактивным контролом.
<g role="button"></g>
const button = svg.append("g")
.attr("role", "button")
.attr("tabindex", 0);
Важно добавлять:
tabindex="0"
иначе клавиатурная навигация не будет работать.
aria-labelaria-label задаёт текстовое описание элемента.
Пример:
<svg aria-label="График продаж за 2025 год"></svg>
Screen reader озвучит:
«График продаж за 2025 год»
aria-label в D3.jsconst svg = d3.select("body")
.append("svg")
.attr("role", "img")
.attr("aria-label", "Столбчатая диаграмма продаж");
D3.js особенно удобен для генерации динамических описаний.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("aria-label", d => `Продажи: ${d.value}`);
Каждый столбец получает индивидуальное описание.
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 8)
.attr("role", "button")
.attr("tabindex", 0)
.attr("aria-label", d => `Точка данных ${d.name}`);
aria-labelledbyПозволяет связать SVG с существующим текстовым элементом.
Пример:
<svg aria-labelledby="chart-title"></svg>
<h2 id="chart-title">Диаграмма температуры</h2>
<title><svg aria-labelledby="title-id">
<title id="title-id">Продажи компании</title>
</svg>
Это более семантичный вариант, чем прямой
aria-label.
<title><title> задаёт краткое название SVG.
Он:
<title><svg>
<title>График населения</title>
</svg>
<title> в
D3.jssvg.append("title")
.text("График населения");
<title><title> желательно размещать первым дочерним
элементом SVG.
Правильно:
<svg>
<title>...</title>
<rect></rect>
</svg>
При наведении курсора браузер показывает содержимое
<title>.
Это поведение зависит от браузера.
<desc><desc> содержит подробное описание SVG.
Если <title> — краткое имя, то
<desc> — полноценное текстовое пояснение.
<desc><svg>
<title>Диаграмма продаж</title>
<desc>
Столбчатая диаграмма показывает рост продаж
с января по июнь.
</desc>
</svg>
<desc> в D3.jssvg.append("desc")
.text(`
Линейный график отображает изменение
температуры по месяцам.
`);
title и
desc| Элемент | Назначение |
|---|---|
title |
Краткое название |
desc |
Подробное описание |
<svg
role="img"
aria-labelledby="chart-title chart-desc">
<title id="chart-title">
Продажи по кварталам
</title>
<desc id="chart-desc">
Диаграмма показывает увеличение продаж
во втором и третьем квартале.
</desc>
</svg>
const svg = d3.select("body")
.append("svg")
.attr("width", 800)
.attr("height", 400)
.attr("role", "img")
.attr("aria-labelledby", "chart-title chart-desc");
svg.append("title")
.attr("id", "chart-title")
.text("Продажи по кварталам");
svg.append("desc")
.attr("id", "chart-desc")
.text(`
Столбчатая диаграмма показывает рост
продаж во втором квартале.
`);
Иногда требуется озвучивание каждой точки данных.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => x(d.name))
.attr("y", d => y(d.value))
.attr("width", x.bandwidth())
.attr("height", d => height - y(d.value))
.attr("tabindex", 0)
.attr("role", "img")
.attr(
"aria-label",
d => `${d.name}: ${d.value} продаж`
);
SVG-элементы не получают фокус автоматически.
Для клавиатурной навигации используется:
tabindex="0"
svg.selectAll("circle")
.attr("tabindex", 0);
Теперь элементы можно выбирать через клавишу Tab.
Интерактивный SVG обязан поддерживать клавиатуру.
svg.selectAll("circle")
.on("keydown", function(event, d) {
if (event.key === "Enter") {
console.log(d);
}
});
Скрывает элемент от screen reader.
aria-hidden="true"
svg.append("path")
.attr("aria-hidden", "true");
Подходит для:
<g>Группы SVG также могут быть доступны.
const group = svg.append("g")
.attr("role", "group")
.attr("aria-label", "Серия данных 2025");
legend
.attr("role", "group")
.attr("aria-label", "Легенда диаграммы");
Оси часто перегружают screen reader.
Иногда их скрывают:
axisGroup.attr("aria-hidden", "true");
const svg = d3.select("body")
.append("svg")
.attr("width", 700)
.attr("height", 400)
.attr("role", "img")
.attr(
"aria-labelledby",
"sales-title sales-desc"
);
svg.append("title")
.attr("id", "sales-title")
.text("Продажи по регионам");
svg.append("desc")
.attr("id", "sales-desc")
.text(`
Столбчатая диаграмма показывает
продажи в северном, южном
и западном регионах.
`);
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => x(d.region))
.attr("y", d => y(d.sales))
.attr("width", x.bandwidth())
.attr("height", d => height - y(d.sales))
.attr("tabindex", 0)
.attr("role", "img")
.attr(
"aria-label",
d => `Регион ${d.region}, продажи ${d.sales}`
);
<svg aria-hidden="true"></svg>
<svg
role="img"
aria-label="Настройки">
</svg>
Плохо:
<svg></svg>
Хорошо:
<svg role="img"></svg>
Плохо:
<svg role="img"></svg>
Хорошо:
<svg
role="img"
aria-label="Диаграмма доходов">
</svg>
Плохо:
svg.selectAll("*")
.attr("aria-label", "...");
Screen reader начинает читать сотни элементов.
Большие SVG могут содержать тысячи DOM-узлов.
Избыточные ARIA-атрибуты:
Поэтому:
aria-hidden.| Инструмент | Назначение |
|---|---|
| Lighthouse | Аудит accessibility |
| axe DevTools | Проверка ARIA |
| NVDA | Screen reader |
| VoiceOver | Screen reader macOS |
| WAVE | Анализ доступности |
Lighthouse проверяет:
Браузер строит дерево доступности отдельно от DOM.
Неправильная семаника SVG приводит к:
Использовать:
role="img"<title><desc>aria-labelledbyДобавлять:
tabindex="0"aria-labelИспользовать:
aria-hidden="true"
или:
role="presentation"
<svg
role="img"
aria-labelledby="chart-title chart-desc">
<title id="chart-title">
Название диаграммы
</title>
<desc id="chart-desc">
Подробное описание диаграммы
</desc>
<g role="group">
</g>
</svg>
D3.js предоставляет полный контроль над DOM, поэтому accessibility
реализуется напрямую через .attr().
selection.attr("role", "img")
selection.attr("aria-label", "Описание")
selection.attr("tabindex", 0)
SVG с корректной семаникой помогает соответствовать:
Особенно важны критерии:
| WCAG | Описание |
|---|---|
| 1.1.1 | Текстовые альтернативы |
| 2.1.1 | Доступность с клавиатуры |
| 4.1.2 | Name, Role, Value |
Хорошая архитектура SVG обычно строится по уровням:
| Уровень | Семаника |
|---|---|
| SVG-контейнер | role, title, desc |
| Группы | role="group" |
| Интерактивные элементы | button, tabindex,
aria-label |
| Декоративные элементы | aria-hidden |
const svg = d3.select("#chart")
.append("svg")
.attr("width", 900)
.attr("height", 500)
.attr("role", "img")
.attr(
"aria-labelledby",
"revenue-title revenue-desc"
);
svg.append("title")
.attr("id", "revenue-title")
.text("Доход компании по месяцам");
svg.append("desc")
.attr("id", "revenue-desc")
.text(`
Линейная диаграмма показывает
рост дохода с января по декабрь.
`);
const bars = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => x(d.month))
.attr("y", d => y(d.value))
.attr("width", x.bandwidth())
.attr("height", d => height - y(d.value))
.attr("tabindex", 0)
.attr("role", "img")
.attr(
"aria-label",
d => `${d.month}, доход ${d.value}`
);
bars.on("keydown", (event, d) => {
if (event.key === "Enter") {
console.log(d);
}
});