Кратко
СкопированоМы привыкли, что в структуре сайта CSS отвечает только за визуальное представление, а всё касающееся контента задаётся в HTML. Однако, это не совсем так. В CSS есть свойства, способные как улучшить восприятие вашего сайта для пользователя вспомогательных технологий, так и сильно усложнить ему жизнь.
В статье разберёмся, что это за свойства и почему так происходит.
Важные уточнения
СкопированоБольшая часть материала в статье посвящена влиянию CSS на скринридеры.
В Доке есть отдельная статья про скринридеры. Здесь я только кратко процитирую, что такое скринридер.
Скринридер (screen reader) — программа, которая превращает контент интерфейсов в речь или шрифт Брайля.
Скринридеры нужны людям со слепотой и слабовидящим, а также пользователям с когнитивными особенностями, которым легче воспринимать информацию на слух. Например, людям с дислексией.
Также в тексте статьи много демок. Протестируйте их сами. Установите или включите скринридер — в статье про скринридеры есть список существующих программ для каждой операционной системы. Инструкции по скачиванию и подключению обычно есть на сайтах скринридеров.
Наконец, все демки я протестировала со скринридером NVDA в Windows в Google Chrome. Если тестируете с другим скринридером, его поведение может немного отличаться от описанного в статье.
Об основном договорились, теперь можно двигаться дальше 🙂
Списки
СкопированоЧтобы не рассматривать скучные абстрактные примеры, давайте представим, что нам нужно пойти в магазин, купить кучу всего и ничего не забыть.
В этом нам поможет приложение для покупок — именно его мы увидим во всех демках.
Первое, что нужно сделать — составить список покупок.
Допустим, в этом случае нам не важно, сколько пунктов в списке, поэтому мы используем ненумерованный список — <ul>
.
Свойство list-style: none
СкопированоПервое, что мы обычно делаем с ненумерованным списком, — убираем стандартные буллиты с помощью свойства list
. Кажется, что на скринридер такое изменение влиять не должно, ведь оно касается только визуального представления списка. Однако на практике это не так.
У обычного списка с буллитами NVDA сначала озвучивает количество элементов в списке, а затем перед каждым элементом произносит слово «маркер». Это даёт пользователю понять, что он перешёл к следующему пункту списка. Например:
Список из четырёх элементов. Маркер. Апельсины. Маркер. Хлеб…
У списка с list
количество элементов произносится, но слово «маркер» опускается. Элементы списка просто зачитываются подряд:
Список из четырёх элементов. Апельсины. Хлеб…
В случае со скринридером VoiceOver свойство list
приведёт к ещё большей путанице. В Safari список с list
вовсе не озвучивается как список. Мы не услышим ни количество элементов, ни слово «маркер».
Если список нумерованный (<ol>
), то для каждого пункта списка скринридер зачитывает порядковый номер, например:
Один. Апельсины. Два. Хлеб.
В случае с list
этот номер игнорируется.
Кастомные маркеры
СкопированоПолучается, что совсем без маркера оставлять список как-то нехорошо. Чаще всего для списков верстают кастомные маркеры. Рассмотрим два распространённых способа это сделать.
Псевдоэлемент ::before
СкопированоСделаем список повеселее, вместо стандартного маркера вставим эмодзи канцелярской кнопки — «📌».
Это можно сделать с помощью псевдоэлемента :
у элемента списка <li>
:
.list_emoji li::before { content: '📌'; display: block; position: absolute; top: -3px; left: -22px;}
.list_emoji li::before { content: '📌'; display: block; position: absolute; top: -3px; left: -22px; }
В свойстве content
у псевдоэлемента указано его содержимое (эмодзи кнопки), а остальные свойства помогают спозиционировать :
относительно элемента списка.
Посмотрим на получившийся «нарядный» список. В демке он под заголовком «:
».
А как это звучит?
Скринридер озвучивает содержимое свойства content
, поэтому перед каждым пунктом списка мы слышим название эмодзи:
Канцелярская кнопка. Апельсины. Канцелярская кнопка. Хлеб…
content
— ещё одно CSS-свойство, влияющее на поведение скринридера. Его содержимое почти всегда будет зачитано, если скринридер найдёт там что-то читабельное. Это может быть текст, цифры или эмодзи.
К счастью, громоздкую конструкцию, например, с адресом ссылки на картинку, NVDA не зачитает 🙂 В этом примере скринридер пропустит значение свойства content
и не будет его читать:
.local-link::before { content: url("/media/examples/firefox-logo.svg");}
.local-link::before { content: url("/media/examples/firefox-logo.svg"); }
Псевдоэлемент ::marker
СкопированоЕщё один вариант создания кастомных маркеров — псевдоэлемент :
.
Снова глянем на демку — теперь нас интересует список с маркерами-галочками.
В этом случае NVDA не будет зачитывать содержимое псевдоэлемента и опустит слово «маркер» перед элементом списка. То есть, как и в примере с list
без кастомных маркеров, снова получим:
Список из 4 элементов. Апельсины. Хлеб…
Значит, псевдоэлемент :
влияет только на внешний вид списка.
Бонусный фан-факт
СкопированоВ этой демке есть ещё один интересный элемент — кнопка. Она выглядит совершенно обычно, но нам важно, что все буквы здесь капитализированы с помощью свойства text
.
.button-uppercase { text-transform: uppercase;}
.button-uppercase { text-transform: uppercase; }
Оказывается, раньше VoiceOver читал капитализированный текст по буквам. Из нашей кнопки получилось бы «Д.О.Б.А.В.И.Т.Ь.П.У.Н.К.Т.» 😱
Одно время в сообществе кипели обсуждения баг это или нет, но в какой-то момент такое поведение всё-таки признали багом и исправили. Сейчас с таким столкнуться уже невозможно.
Свойство order
СкопированоСписок собрали и теперь самое время отправляться за покупками. Допустим, в нашем магазине карточки товаров выглядят как кнопки с эмодзи и текстами.
В демке обычный блок с display
. Попробуем озвучить весь список товаров скринридером.
Ожидание:
Апельсины. Молоко. Воздушный змей. Сок. Брецель. Яблоки.
Реальность:
Апельсины. Яблоки. Молоко. Сок. Брецель. Воздушный змей.
Кажется, что скринридер хаотично изменил порядок товаров. Даже без озвучки при прохождении списка с помощью клавиатуры видно, что фокус как будто прыгает по случайным элементам. На самом деле это не так.
Если заглянуть в код демки, мы увидим, что элементы в HTML стоят ровно в том порядке, в каком их читает скринридер. При этом каждому из них задано свойство order
. Как раз оно и определяет их визуальный порядок.
reading-order-items
СкопированоЭто свойство из черновика CSS Display Module Level 4. Пока оно даже не определилось с тем, как точно называется. Может reading
, может reading
, а ещё лучше reading
?
Свойство предлагают использовать для решения проблемы с order
, которое изменяет визуальный порядок флекс- и грид-элементов и никак не изменяет логический. С его помощью сможете управлять порядком озвучивания элементов скринридером или фокусом с клавиатуры.
На практике reading
будет выглядеть так:
<div class="wrapper"> <a href="#">Первый элемент</a> <a href="#">Второй элемент</a> <a href="#">Третий элемент</a></div>
<div class="wrapper"> <a href="#">Первый элемент</a> <a href="#">Второй элемент</a> <a href="#">Третий элемент</a> </div>
.wrapper { display: flex; flex-direction: row-reverse; reading-order-items: flex-visual;}
.wrapper { display: flex; flex-direction: row-reverse; reading-order-items: flex-visual; }
text-overflow
СкопированоСвойство text
визуально обрезает часть текста, когда он полностью не помещается в доступную область.
Это свойство никак не влияет на скринридеры, и они прочтут текст целиком. Однако зрячие пользователи столкнутся с проблемами при увеличении интерфейса или на небольших экранах. Из-за этого им будет трудно ориентироваться в интерфейсе и понять, что скрывается за многоточием. Возможно, одна строчка текста не нанесёт большого ущерба, чего не скажешь про большие тексты.
Представим, что хотим показать срочное сообщение и, при этом, сохранить ещё пару пикселей в интерфейсе.
<p> Срочное сообщение, от которого зависит всё. С вашего счёта списан 1 000 000 000 галлактических кредитов.</p>
<p> Срочное сообщение, от которого зависит всё. С вашего счёта списан 1 000 000 000 галлактических кредитов. </p>
p { padding: 55px 40px; inline-size: 65%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background-color: #10F3AF; color: #000000;}
p { padding: 55px 40px; inline-size: 65%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background-color: #10F3AF; color: #000000; }
Скорее всего, если только у вас не огромный экран, такое сообщение заинтригует, но не даст нужной информации. На мобильных экранах интрига нарастает ещё больше.
display: table
и display: grid
СкопированоИтак, мы закупились в магазине согласно нашего списка и получили чек с перечнем купленного.
Первый вариант чека — стандартная таблица, свёрстанная с помощью тега <table>
. Она выглядит как таблица и крякает читается скринридером как таблица.
При чтении этой таблицы скринридер честно расскажет нам, сколько в ней строк и столбцов:
Таблица из 4 строк и 3 столбцов.
Также для каждой ячейки будет уточнять название и номер столбца, в котором она находится:
Продукт. Столбец 1. Апельсины. Количество. Столбец 2. 1 килограмм.
А ещё при переходе на новую строку озвучит номер строки:
Строка 3. Продукт. Столбец 1. Молоко.
Скринридеры прекрасно работают с таблицами, поэтому здесь никаких вопросов — всё читается так, как мы ожидали. Но верстать таблицы мало кто любит. А что, если сверстать то же самое, но с помощью display
?
Посмотрим на второй вариант чека. Визуально всё выглядит почти в точности так же, как и обычная таблица. Но внутри теперь не семантический тег <table>
, а обычные <div>
и <p>
. Ожидаемо такой компонент не будет читаться как таблица. В итоге мы услышим простое озвучивание контента блоков:
Продукт. Количество. Цена. Апельсины. 1 килограмм.
Окей, у таблицы есть свойство display
. Может, всё дело в нём? Попробуем сверстать псевдо-таблицу с помощью обычных <div>
, но зададим блоку-обёртке display
. В демке это первый вариант с заголовком «Таблица и display
».
Увы, скринридер всё ещё не считает это таблицей и снова читает только контент внутри блоков в ожидаемом нами порядке:
Продукт. Количество. Цена. Апельсины. 1 килограмм.
Продолжим экспериментировать и теперь навесим свойство display
на настоящую семантическую таблицу. Зачем? Во-первых, почему бы и нет. А во-вторых, чтобы проверить утверждение, которое встречалось мне в нескольких статьях. В этом случае свойство display
должно сломать семантику таблицы.
Проверим на практике. Вторая таблица с заголовком «Контейнеры и display
» из демки свёрстана как таблица с display
у тега <table>
.
На удивление, NVDA справился с такой путаницей отлично. Он прочитал элемент как настоящую таблицу и озвучил все строки и столбцы. Однако стоит помнить, что другие скринридеры могут повести себя в такой ситуации совершенно непредсказуемо. Это значит, что таких сюжетных поворотов в вёрстке лучше избегать 🙂
display: contents
Скопированоcontents
— это значение свойства display
с пока что частичной поддержкой браузерами, благодаря которому можно напрямую применять стили к дочерним элементам внутри контейнера. Звучит как магия, но есть одно «но». Все элементы внутри контейнера с display
теряют свою семантику.
.container { display: contents;}
.container { display: contents; }
Как видим код мы:
<div class="container"> <p> Наша замечательна рассылка лучшая рассылка среди всех рассылок. </p> <button>Подписаться на рассылку</button></div>
<div class="container"> <p> Наша замечательна рассылка лучшая рассылка среди всех рассылок. </p> <button>Подписаться на рассылку</button> </div>
Как видят код скринридеры:
<span> Наша замечательна рассылка лучшая рассылка среди всех рассылок.</span><span>Подписаться на рассылку</span>
<span> Наша замечательна рассылка лучшая рассылка среди всех рассылок. </span> <span>Подписаться на рассылку</span>
Спрятанное содержимое
СкопированоЕсть много способов скрыть контент страницы, но не все из них скрывают контент одновременно и для зрячих пользователей, и для пользователей скринридеров.
В Доке есть отдельная статья «Как скрыть содержимое от скринридеров». В ней подробно описаны способы скрытия и показа содержимого. Например, только визуально, только для скринридеров или всё вместе. Поэтому здесь только кратко процитируем описание CSS-свойств, которые заставят скринридер замолчать 😈
width
и: 0px height
удаляют элементы из потока страницы, поэтому скринридеры их не прочитают. Не работает с NVDA. Он по-прежнему будет читать такие элементы.: 0px visibility
скрывает содержимое тега, но оставляет элемент в обычном потоке страницы таким образом, что он по-прежнему занимает место.: hidden display
полностью удаляет элемент из документа. Он не занимает места, хотя всё ещё находится в исходном HTML-коде.: none
Не используйте эти CSS-стили, если хотите, чтобы содержимое читалось программой чтения с экрана. Помните, что опыт незрячего пользователя не должен отличаться от опыта зрячего. Это важно для пользователей с частичными нарушениями зрения, которые используют скринридер не всё время, а только в некоторых ситуациях.
Анимации и prefers-reduced-motion
СкопированоУра, кажется, мы всё купили и теперь можем посмотреть на красивый экран с анимацией 🎉
Однако, на доступном сайте должна быть возможность отключить анимацию, если пользователю это важно. Такое поведение описано в одном из требований WCAG 2 (Web Content Accessibility Guidelines 2) — 2.3.3. Анимация при взаимодействии.
У CSS-директивы @media
есть значение, которое позволяет влиять на анимации в зависимости от настроек системы пользователя — prefers
. Если в операционной системе пользователя отключена анимация, то на сайте будет выполнен CSS-код внутри директивы.
Например, в этом коде анимация в prefers
выключена совсем:
.animated-element { animation: rotation 1.6s infinite;}@media (prefers-reduced-motion) { .animated-element { animation: none; }}
.animated-element { animation: rotation 1.6s infinite; } @media (prefers-reduced-motion) { .animated-element { animation: none; } }
В демке уже написаны стили, отключающие анимацию при prefers
. Это можно проверить, отключив отображение анимации у себя на компьютере или смартфоне в настройках системы.
- Для Windows: Параметры → Специальные возможности → Другие параметры → Воспроизводить анимацию в Windows.
- Для macOS: Системные настройки → Универсальный доступ → Монитор → Уменьшить движение.
- Для Linux: Настройки → Специальные возможности → Разрешить анимацию.
- Для iOS: Настройки → Универсальный доступ → Движение → Уменьшение движения.
- Для Android: Настройки → Специальные возможности → Экран → Удалить анимации.
Если ваш браузер — Google Chrome, то можно имитировать эту настройку в инструменте разработчика: «Другие инструменты» (More tools) → вкладка «Отрисовка» (Rendering) → «Эмулировать медиафункцию CSS prefers-reduce-motion» (Emulate CSS media feature prefers-reduce-motion).
При активированном режиме «уменьшенной анимации» хлопушка в демке не должна двигаться. При отключении этого режима она начнёт двигаться снова.
Кстати, в Доке уже есть материал о prefers
.
Почему стили влияют на доступность?
СкопированоКак скринридер решает, какие элементы он будет читать, а какие пропустит?
Дело в том, что скринридер читает не просто контент страницы или её разметку, а общается с браузером при помощи Accessibility API (Accessibility Application Programming Interface).
Accessibility API передаёт скринридеру данные о странице в виде дерева доступности (accessibility tree). Оно похоже на DOM-дерево, но состоит из доступных объектов (accessible object). То есть, именно Accessibility API превращает разметку страницы в «сценарий чтения». Как этот сценарий будет выглядеть зависит от многих факторов. Например, от семантической разметки и используемых CSS-свойств.
Выводы
СкопированоCSS тоже может влиять на то, как контент страницы будет прочитан скринридером.
- Свойство
list
превращает семантический список в обычный перечень элементов. Скринридер не скажет сколько всего элементов и не будет обозначать каждый новый элемент словом «маркер» в случае с- style : none <ul>
или порядковым номером элемента в случае с<ol>
. - Содержимое псевдоэлементов
:
и: before :
зачитывается скринридером. Если внутри свойства: after content
ссылка, он её не прочитает. - Содержимое псевдоэлемента
:
невидимо для скринридера, но список с таким псевдоэлементом всё равно не читается как список, если ему задан: marker list
.- style : none - Свойство
order
меняет порядок элементов только визуально. Скринридер будет читать элементы в том порядке, в котором они расположены в разметке. - Новое свойство
reading
в будущем поможет победить- order - items order
. text
не создаёт проблемы для вспомогательных технологий, но создаёт проблемы для всех остальных.- overflow display
не сделает для скринридера таблицу из обычных: table <div>
-контейнеров.display
у: grid <table>
вовсе может сломать всю семантику для некоторых скринридеров.display
ломает семантику в старых версиях браузеров до 2019 года.: contents display
и: none visibility
позволяют скрыть контент как визуально, так и от скринридеров.: hidden width
и: 0px height
тоже скрывают контент, но не для всех скринридеров. Например, NVDA прочитает блок, спрятанный таким образом.: 0px - С помощью директивы
@media
со значениемprefers
можно предоставить фолбэк-стили на случай, если у пользователя в системе выключена анимация.- reduced - motion
Сложно заранее представить, как скринридер озвучит интерфейс, даже если прочитали кучу статей о вспомогательных технологиях и, кажется, знаете уже все подводные камни. Единственный способ проверить как действительно звучит содержимое — сесть и протестировать его с любым скринридером вручную. Это всегда полезно, а часто ещё и довольно весело.
Узнать больше об особенностях влияния CSS на скринридеры можно в этих статьях: