Привет, Хабр. Я продолжаю отвечать на вопросы по вёрстке из собеседований на должность фронтендера. Если я где-то ошибаюсь, вы поправляете меня в комментариях. Таким способом я учусь у вас, а вы у меня.


Сегодня я отвечу на следующий вопрос: «Как скрыть элемент с помощью CSS доступно?»


▍ Введение


Для объяснения мне потребуются «инструменты разработчика» в браузере. А точнее, возможность отображать дерево доступности (Accessibility tree). Что это такое?


Дерево доступности — это форма представления HTML-документа, как DOM, которую понимают вспомогательные технологии — такие, как скринридеры.


Включить его можно в devTools. Для демонстрации я создам страницу.


<body>
  <main class="page">
    <div class="content">
      <h1>Дерево доступности</h1>
      <p>
        Дерево доступности — это представление элементов документа в виде дерева на
        <a href="https://ru.wikipedia.org/wiki/Document_Object_Model">основе объектной модели документа</a>.
      </p>
    </div>		
  </main>
</body>

Откроем devTools. Во вкладке с разметкой будет кнопка в виде человечка.


В инструментах разработчика отображается код страницы

Нажмём на неё.


В инструментах разработчика отображается дерево доступности

Теперь мы видим дерево доступности. Добавлю HTML-элементы, чтобы было более понятно, где какой элемент.


Элемент main создает узел main, элемент div создает узел generic, элемент h1 создает узел heading, элемент p создает узел paragraph, элемент a создает узел link, текст создает узел static text

Для достижения целей статьи нам не требуется изучать, почему разметка отобразилась так. Нам будет достаточно увидеть изменения дерева. Однако если вам интересно, почему же дерево такое, то гуглите про атрибут role. У меня тоже есть статья о нём.


Также нужно отметить, что дерево доступности в devTools не гарантирует правильный результат. У вспомогательных технологий есть свои баги, влияющие на итоговый результат.


Всё. Мы готовы двигаться дальше. Рассмотрим, как с помощью CSS мы можем скрыть элемент, и какой из способов позволяет сделать это доступно.


▍ Свойство display и значение none


Начнём мы со способа, который чаще всего называют на собеседовании. Добавим свойство display со значением none к элементу .content.


.content {
  display: none;
}

Отображается только узел main

Дерево доступности изменилось. Почему так?


После того как значение none применяется к элементу, он и его потомки удаляются из дерева доступности. По этой причине вспомогательные технологии не могут найти их. Также у интерактивных элементов теряется свойство интерактивности. Нельзя попасть на него с помощью клавиши Tab. Для клика он тоже недоступен.


Этот случай является примером полной «недоступности» элемента. И это его плюс! Бывают же случаи, когда временно элемент скрыт. Например, многоуровневые меню, модальные окна.


Здесь без значения none не обойтись. Оно гарантирует, что у вспомогательных технологий нет преждевременного доступа к элементам.


▍ Свойство display и значение contents


При значении contents скрывается сам элемент, но его контент остаётся визуально отображаемым. Для демонстрации данного эффекта я добавлю красный фон к элементу с классом .content.


.content {
  display: contents;
  background-color: red;
}

Элемент div с красным фоном скрыт и визуально не заметен
В дереве доступности пропал узел generic, который был создан элементом div

Элемент со свойством display и значением contents пропал. По этой причине он сам становится недоступным для вспомогательных технологий. Но его потомки, элементы <h1>, <p> и <a>, доступны без проблем.


К отрицательным моментам данного способа относится случай с интерактивными элементами. Если к ним применено значение contents, то элементы потеряют свою интерактивность. Вместо них останется просто текст, потому что он является их потомком. Больше деталей я рассказал в другой статье.


▍ Свойство visibility и значение hidden


Данный способ похож на метод со свойством display и значением none.


.content {
  visibility: hidden
}

Отображается только узел main

Элемент .content и его дочерние элементы невидимы для вспомогательных технологий, потому что в дереве доступности их нет. На интерактивные элементы нельзя попасть с помощью клавиатуры. Мышкой кликнуть по ним тоже не получится.


Всё точно так же, как при значении none у свойства display. И плюсы такие же. Не буду повторяться.


▍ Свойство opacity и значение 0


Скрыть элемент с помощью свойства opacity является вторым по распространённости ответом, который я слышу. Посмотрим, что случится после добавления его в наш пример.


.content {
  opacity: 0;
}

Все узлы в дереве доступности отображены

В дереве доступности у нас не будет изменений. Все элементы отобразятся. Вроде вот он, идеальный способ скрыть элемент. К сожалению, это обманчивое впечатление.


Некоторые вспомогательные технологии поймут, что им нужно скрыть элемент со свойством opacity и значением 0. Следовательно, весь наш контент станет недоступен для пользователей.


Дополнительным важным нюансом такого способа является то, что интерактивные элементы сохраняют своё поведение. Они доступны для пользователей клавиатуры. А ещё мы можем навести на них мышкой и кликнуть.


В нашем примере может получиться так, что пользователь попадёт на ссылку, но даже не поймёт этого.


▍ Скрыть элемент за пределы вьюпорта браузера


Такая техника основана на сдвиге элемента за пределы вьюпорта с помощью свойств top или left. У него должно быть установлено свойство position со значением absolute или fixed.


.content {
  position: absolute;
  left: -100vw;
}

Все узлы в дереве доступности отображены

С точки зрения дерева доступности, данный способ нормальный. Все элементы будут найдены. Только тут есть ряд проблем.


При использовании клавиши Tab я попаду на ссылку из нашего примера, но я не увижу её! Я подумаю: «Чёрт, что за фигня. Мне придётся дополнительно разбираться, почему я жму Tab и не попадаю на ожидаемый мной элемент».


Также есть косяк, если такой подход применить к элементам формы. Например, к чекбоксу. Вот что получается.


Подсказка браузера, что нужно отметить чекбокс отображена в левом верхнем углу окна

Поскольку элемент сдвинут за пределы вьюпорта, то подсказка прижата к границе. А это не худший сценарий. Так получилось, потому что я открыл демонстрацию в режиме с элементом <iframe>. Без него я не увижу подсказку вообще.


Так что если интерактивный элемент доступен в дереве доступности, то браузер везде найдёт его реальную позицию. Следовательно, он привяжется к ней.


Кстати, пару лет назад скринридеры прямо говорили, что элемент находится за пределами окна. Правда, у меня не получилось вспомнить, кто так делал. Мои знакомые эксперты сказали, что такого уже нет. Но я бы не был так уверен.


▍ Паттерн vissualy-hidden


Получается, нет свойства, позволяющего визуально скрыть элемент, но сохранить его доступ для вспомогательных технологий. Поэтому придумали универсальный паттерн vissualy-hidden. У него множество вариаций, но базовый набор свойств следующий:


.vissualy-hidden {
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
}

Он учитывает несколько требований. Первое, визуально скрыть любой элемент с сохранением его доступности для вспомогательных технологий. Они должны позволить пользователю взаимодействовать с ним и понять его контент.


За это отвечают свойства width, height, clip-path и overflow. Они создают размеры 1 пиксель на 1 пиксель, а через свойство clip-path обрезают его. Свойство overflow контролирует, чтобы ничего не отобразилось за его пределами. Честно, я не знаю, как может это случится. Ведь свойство clip-path обрезает края. Но всё же я оставляю свойство overflow на всякий случай.


Вторая задача заключается в том, чтобы вытащить элемент из контекста документа, чтобы он не влиял на своё окружение. Здесь за работу берётся свойство position со значением absolute. Почему не fixed? Иногда требуется сделать так, чтобы элемент располагался относительно родителя. Со значением fixed так не получится.


Осталось свойство white-space. Когда перемещаешься по странице через скринридер, он обводит текущий элемент обводкой по его размеру. Указав свойства width и height, мы сжали элемент и текст в нём. Обводка тоже будет сжата за ними. Вот свойство white-space исправляет это поведение, делая обводку обычной прямоугольной формы по границы текста.


В итоге паттерн можно использовать для любых задач. Важно помнить, что в случае интерактивных элементов, в моменте, когда они получают фокус, их нужно отобразить, переопределив значения свойств width, height и clip-path.


▍ Заключение


Отвечу кратко на вопрос: «Как скрыть элемент с помощью CSS доступно?».


Важный шаг, который нужно сделать в своём ответе, это уточнить, что собеседник имеет в виду под словом «доступно». Для доступного интерфейса фронтендеру нужно знать, как скрыть элемент так, чтобы к нему у пользователя вспомогательных технологий не было доступа. Или, наоборот, когда доступ нужно сохранить. Оба сценария абсолютно важны и напрямую влияют на то, как люди смогут использовать продукт.


В качестве примера первого случая можно использовать элементы, которые появляются после действий пользователя. Например, аккордеоны, подменю, модальные и диалоговые окна, попапы. Здесь свойства display и visibility со значениями none и hidden отличное работают.


Если в задаче требуется скрыть элемент, чтобы он визуально не был заметен, но вспомогательные технологии нашли его, то здесь правильный выбор будет паттерн vissualy-hidden.


Он поможет дать больше вспомогательной информации пользователю, добавить дополнительные элементы управления (Паттерн «Skip-link»), скрыть нативные элементы управления при создании кастомных (чекбоксы и радиокнопки).


Дополнительно у нас есть возможность скрыть элемент с сохранением видимости его контента. Значение contents позволяет это сделать. Но есть ряд случаев, когда его применение приводит к негативному опыту.


Точно стоит избегать использования методов, отображающих элементы за границами окна браузера, скрывающие элемент с помощью свойства opacity. Они приводят к неожиданным для пользователя результату.


Оставляю ссылки на все выпуски:

Если вам была интересна статья и вы хотите, чтобы я продолжил эту серию, то напишите, пожалуйста, вопросы, которые сбивали вас с толку на собеседовании. Я попробую ответить на них в новой статье. Спасибо за чтение.


P.S. Помогаю больше узнать про CSS в своём ТГ канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.


Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (6)


  1. vanxant
    11.06.2024 17:20

    Вы делаете большое дело, но всё-таки насчёт собеседований вы имхо немного поторопились.

    А так, конечно, странно, что нет какого-нибудь aria-* для этого случая.


  1. bakhirev
    11.06.2024 17:20

    А есть какой-то практический случай, зачем вообще нужно такое поведение?


  1. Extremum
    11.06.2024 17:20

    Еще можно попробовать для скрытия: transform: scale(0); clip-path: inset(50%); filter: blur(1000px) - последнее конечно спорный подход, но как лабораторный эксперимент...


  1. ALapinskas
    11.06.2024 17:20
    +3

    Всё точно так же, как при значении none у свойства display. И плюсы такие же. Не буду повторяться.

    Все же они немного отличаются в визуальном плане. display:none убирает элемент со страницы полностью, а visibility:hidden - скрывает содержимое, как если применить opacity:0;


  1. DownHouse
    11.06.2024 17:20

    Че-то нет никаких человечков в девтулсе


  1. muxa_ru
    11.06.2024 17:20

    Судя по тексту, фронтендеру не нужно знать о том, что браузеры бывают разные. :)