Кратко
СкопированоМы привыкли, что в структуре сайта 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
. Как раз оно и определяет их визуальный порядок.
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>
Сделаем скидку на то, что это пока что относительно новое значение display
, но лучше не использовать его до того, как баг с потерей семантики починят в браузерах.
Как спрятать содержимое?
СкопированоЕсть много способов скрыть контент страницы, но не все из них скрывают контент одновременно и для зрячих пользователей, и для пользователей скринридеров.
В Доке есть отдельная статья «Как скрыть содержимое от скринридеров». В ней подробно описаны все способы скрытия и показа содержимого. Например, только визуально, только для скринридеров или всё вместе. Поэтому здесь я только кратко процитирую описание 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
меняет порядок элементов только визуально. Скринридер будет читать элементы в том порядке, в котором они расположены в разметке. display
не сделает для скринридера таблицу из обычных: table <div>
-контейнеров.display
у: grid <table>
вовсе может сломать всю семантику для некоторых скринридеров.display
и: none visibility
позволяют скрыть контент как визуально, так и от скринридеров.: hidden width
и: 0px height
тоже скрывают контент, но не для всех скринридеров. Например, NVDA прочитает блок, спрятанный таким образом.: 0px - С помощью директивы
@media
со значениемprefers
можно предоставить фолбэк-стили на случай, если у пользователя в системе выключена анимация.- reduced - motion
Сложно заранее представить, как скринридер озвучит интерфейс, даже если прочитали кучу статей о вспомогательных технологиях и, кажется, знаете уже все подводные камни. Единственный способ проверить как действительно звучит содержимое — сесть и протестировать его с любым скринридером вручную. Это всегда полезно, а часто ещё и довольно весело.
Узнать больше об особенностях влияния CSS на скринридеры можно в этих статьях: