Достъпност
Накратко: Достъпността (accessibility) значи страницата да работи и с клавиатура, и с екранен четец: браузърът строи от HTML дърво с роля, име и състояние на всеки елемент.
Проблем
Бутонът, който не е бутон
Страницата на продукта работи: кликаш „Добави в кошницата“ и продуктът влиза в кошницата. Клиент пише, че не може да купи нищо. Той не ползва мишка: движи се с клавиатурата, а екранният четец му казва какво има на страницата.
Конзола
Кликни в полето „Количество“ и натисни Tab. Фокусът прескача „Добави в кошницата“ и отива на сърцето. Сърцето е бутон, но екранният четец казва само „бутон“: какво прави, не се знае. Двата елемента изглеждат наред. Грешката е в това, което браузърът не рисува.
Идеята
Второто дърво
От DOM браузърът строи още едно дърво: дървото на достъпността (accessibility tree). То е страницата, както я виждат хората и програмите, които не гледат екрана:
- екранният четец чете страницата на глас на човек, който не вижда;
- гласовото управление: човек казва „натисни Добави в кошницата“ и програмата търси бутон с това име;
- клавиатурата: Tab минава по елементите, които могат да се натиснат;
- автоматичните тестове като Testing Library търсят по роля и име. Така проверява и пясъчникът тук.
Всеки възел в това дърво има три неща:
- роля: какво е елементът: бутон, връзка, заглавие, поле, картинка;
- достъпно име: как се казва: „Добави в кошницата“;
- състояние: натиснат ли е, отметнат ли е, изключен ли е.
Ролята идва от тага: <button> е бутон, <a href> е връзка, <h1> е заглавие. <div> и <span> нямат роля. В дървото от тях остава само текстът.
Виж го
Виж двете дървета
Карта на продукт с три „бутона“. Гледай какво остава от всеки вдясно.
<body><article> <h2>Хляб</h2> <img src="bread.svg" alt="Хляб"> <label>Брой <input type="number"></label> <div onclick="add()">Добави</div> <button><svg>…</svg></button> <button>Добави</button></article></body>Стъпка по стъпка, с думи
- Вляво е DOM-ът. От него браузърът строи второ дърво: дървото на достъпността. Него четат екранният четец, гласовото управление и проверките по роля. <article> става ролята article.
- <h2> става heading (заглавие). Името му идва от текста вътре: „Хляб“.
- Картинката няма текст вътре. Името ѝ идва от alt. Без alt картинката е без име.
- Полето получава роля spinbutton (числово поле), а името му идва от <label>: „Брой“. Самият <label> го няма в дървото, има само текста му.
- <div> няма роля. onclick не променя това. Остава само текстът „Добави“. Не е бутон и клавиатурата не стига до него.
- Това е истински бутон, но вътре има само икона без текст. Името е празно. Екранният четец казва само „бутон“.
- <button> с текст: роля button и име „Добави“. Tab стига до него, а Enter и интервалът го натискат.
- getByRole('button', { name: 'Добави' }) търси в дясното дърво. Намира един бутон: истинския. <div>-ът и бутонът без име за нея не съществуват.
Дървото на достъпността се вижда в браузъра: в инструментите за разработчици (F12) отвори Elements и панела Accessibility. Там е ролята и името на избрания елемент.
Идеята
Откъде идва името
Името на елемента се взима по правила. Най-честите източници:
- текстът вътре:
<button>Добави в кошницата</button>,<a href="/cart">Кошница</a>,<h1>; altна картинката:<img src="logo.svg" alt="Магазинът">. Връзка, в която има само картинка, взима името си отalt;<label>на полето: „Количество“ е името на полето до него;aria-label: име, записано направо в атрибута, когато няма видим текст:<button aria-label="Добави в любими">с икона вътре;aria-labelledby: името е текстът на друг елемент, посочен поid.
aria-label и aria-labelledby са по-силни от текста вътре. Името казва какво прави елементът, а не как изглежда: „Добави в кошницата“, а не „количка“. За картинка във връзка името казва къде води връзката: „Магазинът“, а не „лого“.
Пробвай
Дай им имена
Логото горе води към началото, но е картинка без alt. Бутонът с количката е само икона. Дай име на връзката („Магазинът“) и на бутона („Добави в кошницата“). Проверката ги търси по роля и име и натиска бутона.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<title>Прясно мляко 3% · Магазинът</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<a href="/"><img src="img/logo.svg" alt="Магазинът" width="40" height="40"></a>
</header>
<main>
<article>
<h1>Прясно мляко 3%</h1>
<p>Цена: 2,40 €</p>
<button type="button" aria-label="Добави в кошницата" onclick="console.log('Добавено в кошницата: Прясно мляко 3%')">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M3 4h3l2 11h11l2-8H7" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="9" cy="19" r="1.6"/><circle cx="18" cy="19" r="1.6"/></svg>
</button>
</article>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.add, button { font: inherit; padding: 6px 14px; border: 0; border-radius: 6px; background: #1f6f5c; color: white; cursor: pointer; }
.fav { background: none; color: #c2477f; padding: 4px; }
.fav svg { fill: currentColor; }
.add { display: inline-block; }
label { display: block; margin: 8px 0; }
input { width: 4em; }
Логото получава alt="Магазинът" и връзката взима името си от него. Бутонът получава aria-label="Добави в кошницата": в него няма текст, от който да дойде име. alt="лого" не минава: човек, който чуе „лого, връзка“, не знае къде води тя. aria-label="Кошница" също не минава: бутонът не отваря кошницата, а добавя в нея.
Идеята
Клавиатурата и фокусът
Не всеки ползва мишка. Някой има треперещи ръце, някой не вижда курсора, някой пише по-бързо с клавиатура. Той минава по страницата с Tab и натиска с Enter.
Фокусът е елементът, който в момента получава клавишите. Tab го мести напред, Shift+Tab назад. Tab стига само до елементите, които могат да се натиснат: връзки с href, бутони, полета, падащи менюта. Редът е редът в DOM, отгоре надолу.
<button> идва с всичко: роля, фокус, Enter и интервал го натискат. <div> няма нищо от това. Да му добавиш фокус и Enter значи да напишеш наново <button>.
tabindex сменя кой елемент получава фокус:
tabindex="0": елементът влиза в реда на Tab, на мястото си в DOM;tabindex="-1": Tab го прескача, но кодът може да му даде фокус сfocus();tabindex="1"и нагоре: елементът отива най-отпред, преди всички останали. Редът по страницата се чупи. Не го ползвай.
Браузърът рисува рамка около елемента с фокус. Без нея човек с клавиатура не знае къде е. Не я махай. Ако не ти харесва, смени я с друга.
Пробвай
С клавиатурата
„Добави в кошницата“ е <div>. Направи така, че Tab да стига до него след полето „Количество“ и Enter да добавя в кошницата. Проверката натиска Tab два пъти и после Enter.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<title>Хляб „Добруджа“ · Магазинът</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Хляб „Добруджа“</h1>
<p>Цена: 1,80 €</p>
<label>Количество <input type="number" value="1" min="1"></label>
<button type="button" class="add">Добави в кошницата</button>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.add, button { font: inherit; padding: 6px 14px; border: 0; border-radius: 6px; background: #1f6f5c; color: white; cursor: pointer; }
.fav { background: none; color: #c2477f; padding: 4px; }
.fav svg { fill: currentColor; }
.add { display: inline-block; }
label { display: block; margin: 8px 0; }
input { width: 4em; }
script.jsconst add = document.querySelector('.add');
const qty = document.querySelector('input');
add.addEventListener('click', () => {
console.log(`Добавено в кошницата: ${qty.value} × Хляб „Добруджа“`);
});
<div class="add"> става <button type="button" class="add">. Класът остава, затова script.js го намира, а CSS го рисува както преди. Tab стига до бутона след полето, защото е след него в DOM. Enter на бутон пуска click, затова същият слушател работи. <div role="button" tabindex="0"> не минава: екранният четец казва „бутон“, Tab стига до него, но Enter не прави нищо. tabindex="1" на бутона също не минава: Tab отива първо на него, преди полето.
Пробвай
Намери грешката
С мишката бутонът работи. Човек с клавиатура натиска Tab и не вижда къде е. Кой ред е виновен?
Избери реда с грешката.
Ред 3 маха рамката на фокуса. Бутонът получава фокус, но нищо не го показва. Махни реда или сложи друга видима рамка: button:focus-visible { outline: 3px solid #f4c542; }.
Идеята
Заглавия и първото правило на ARIA
Заглавията са съдържанието на страницата. Човек с екранен четец не я чете от началото. Той иска списък на заглавията или скача между тях с клавиш (в NVDA и JAWS е H). Затова нивата казват плана: един <h1> за страницата, <h2> за разделите, <h3> в тях. Не избирай <h4>, защото е по-малко. Размерът се сменя с CSS.
ARIA (Accessible Rich Internet Applications) е набор от атрибути, които сменят дървото на достъпността: role, aria-label, aria-expanded, aria-live и други. Те казват на екранния четец нещо, което HTML не може да каже сам.
ARIA сменя само това, което се обявява, а не поведението. role="button" на <div> кара четеца да каже „бутон“, но Tab пак не стига до него, а Enter пак не прави нищо. Човекът чува „бутон“, натиска и нищо не става. Това е по-лошо от <div> без роля.
Затова първото правило на ARIA е: ако има HTML елемент, който прави това, ползвай него. <button>, а не role="button". <a href>, а не role="link". <nav>, а не role="navigation". ARIA е за случаите, в които HTML няма такъв елемент или няма видим текст, от който да дойде име.
Пробвай
Попълни
Кой атрибут дава име на бутон, в който има само икона?
<button type="button" ="Добави в любими">
<svg viewBox="0 0 24 24">…</svg>
</button>aria-label: името, което екранният четец казва и по което гласовото управление и тестовете намират бутона.
По-точно, за любопитните
Съобщения, които се появяват. След „Добави в кошницата“ страницата пише „Добавено: Хляб „Добруджа““. Който гледа екрана, го вижда. Екранният четец не забелязва промяната, защото фокусът не е там. <p role="status"> (или aria-live="polite") казва: когато текстът тук се смени, прочети го, щом свършиш с текущото. За спешни грешки е role="alert", който прекъсва. Елементът трябва да е в страницата преди съобщението: сменя се само текстът му.
Езикът. <html lang="bg"> казва на екранния четец с какъв глас да чете. С грешен lang българският текст се чете с английско произношение и е неразбираем. Дума на друг език в текста получава свой: <span lang="en">Dobrudzha</span>.
Скриване. display: none и атрибутът hidden махат елемента и от екрана, и от дървото на достъпността. aria-hidden="true" маха само от дървото: за икони до текст, които четецът няма защо да казва. Елемент с фокус в aria-hidden е грешка: Tab стига до него, а четецът мълчи.
Редът на имената. Пълният алгоритъм е стандартът Accessible Name and Description Computation. Накратко: aria-labelledby, после aria-label, после това, което дава HTML (<label>, alt, текстът вътре), и накрая title. title и placeholder не са добри имена: title се вижда само при задържане с мишката, а placeholder изчезва, щом пишеш.
:focus и :focus-visible. :focus важи винаги, когато елементът има фокус, и след клик с мишката. :focus-visible важи, когато браузърът реши, че рамката трябва да се вижда: при Tab, но не и след клик на бутон. Затова, ако искаш да сменяш рамката, ползвай :focus-visible. За CSS е следващият модул.
Скок до съдържанието. Първата връзка на много страници е „Към съдържанието“ (<a href="#main">). Тя е скрита, докато не получи фокус. Така човек с клавиатура не минава всеки път през цялото меню.
Упражнение
Кошницата
Кошницата е направена набързо. „Кошница“ е само удебелен текст, бутоните „×“ са <span>, а връзката „Към поръчката“ има tabindex="1", „за да е първа“. Поправи я: заглавие от първо ниво; бутони, до които Tab стига и които Enter натиска, с имена „Премахни Хляб „Добруджа““ и „Премахни Прясно мляко 3%“; Tab по реда на страницата: първият бутон, вторият, връзката. Проверката минава кошницата само с клавиатурата.
Конзола
Решение
index.html<!doctype html>
<html lang="bg">
<head>
<meta charset="utf-8">
<title>Кошница · Магазинът</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Кошница</h1>
<ul>
<li>Хляб „Добруджа“, 2 × 1,80 € <button type="button" class="remove" aria-label="Премахни Хляб „Добруджа“">×</button></li>
<li>Прясно мляко 3%, 1 × 2,40 € <button type="button" class="remove" aria-label="Премахни Прясно мляко 3%">×</button></li>
</ul>
<p>Общо: 6,00 €</p>
<a href="/checkout">Към поръчката</a>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
.title { display: block; font-size: 1.6em; margin: 8px 0; }
li { margin: 6px 0; }
.remove { font: inherit; margin-left: 8px; padding: 0 8px; border: 1px solid #999; border-radius: 4px; background: white; cursor: pointer; }
script.jsfor (const remove of document.querySelectorAll('.remove')) {
remove.addEventListener('click', () => remove.closest('li').remove());
}
„Кошница“ става <h1>. Всеки <span class="remove"> става <button type="button" class="remove"> с aria-label: видимият текст „ד не казва какво прави бутонът. Името казва и кой продукт маха, защото два бутона „Премахни“ звучат еднакво. tabindex="1" се маха: връзката е последна в DOM и е последна за Tab. Проверката натиска Enter на първия бутон и брои продуктите, затова <span role="button" tabindex="0"> не минава.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: HTML документът · Текст, връзки, картинки, таблици · Формуляри
- Продължава в: Responsive дизайн · Списък от данни · Данни от API · Първият компонент
- Задачи по темата: HTML: структурата на страницата