Сайты с модальными диалоговыми окнами без JavaScript

Сайты, которые используют JavaScript для большей части таких элементов, как модальные диалоговые окна, выпадающее меню, мобильные меню-«гамбургеры», зачастую демонстрируют полный провал в плане удобства пользования и доступности. В особенности это верно, когда речь заходит об отсутствии у них постепенной деградации.

Точно так же многие «классические» методы навигации, к примеру выпадающее меню, неудобны для пользователей с ограниченными возможностями. И в NNGroup, и в основных руководствах (том же WCAG) нам регулярно повторяют: не пользуйтесь для этого скриптами, здесь хватает проблем и без них.

JavaMentor
JavaMentor

CSS3 предоставляет все инструменты, чтобы это исправить. При этом объем кода остается таким же или даже меньшим, а устройства чтения с экрана, считыватели шрифта Брайля, поисковые системы и т. д. способны обработать страницу так же хорошо, как если бы ее составляли только обычные элементы навигации.

И вся эта магия получается благодаря одному простому псевдо-классу :target.

Что такое :target в CSS3?

Этот псевдо-класс позволяет стилизовать элементы, которые в данный момент являются целью хэш-части текущего адреса страницы. Когда этот хэш совпадает либо с именем тега <a>nchor, либо с любым элементом страницы с тем же идентификатором, :target для указанного элемента будет верным.

Именно это применение  —  таргетинг ID для применения стилей  —  нам и понадобится. Допустим, у меня есть:

<div id=”test”>Какой-то текст</div>

И такой CSS:

#test:target { color:red; }

Если в конце URI содержится хэш #test, текст внутри div будет красным. В ином случае будет стоять цвет по умолчанию.

Как же этим воспользоваться, чтобы сделать модальный диалог?

Если элемент не обозначен как :target, спрячьте его. Если наоборот  —  покажите. Вот и всё переключаемое состояние без всяких скриптов.

Разметка

Если по умолчанию необходимо скрыть внешний контейнер модального диалога, а затем показать его поверх содержимого, когда он станет :target, значит для открытия модального диалога нужно только следующее:

<a href=”#ourModal”>Open Our Modal</a>

А чтобы закрыть его, внутри модального диалога можно просто поставить:

<a href=”#”>Close Modal</a>

При изменении хэша на пустое значение, диалог закрывается.

Достаточно поразмыслив над этим, в итоге я остановился на следующей структуре:

<div id="ourModal" class="modal">
  <a href="#" class="modalClose" hidden aria-hidden="true"></a>
  <div><section>
    <a href="#" class="modalClose" hidden aria-hidden="true"></a>
    <h2>Our Modal</h2>
    <div>
      <p>
        A Sample Modal
      </p>
    </div>
  </section></div>
<!-- #ourmodal.modal --></form>

Как и всегда, я старался не разрушать разметку так называемыми “классами ни о чем”, а вместо этого сгрузил большую часть работы на семантику и структуру DOM.

Назначение внешнего div очевидно: это будет “фиксированный” контейнер, установленный на весь экран. display:flex; в сочетании с align-items:center; и justify-content:center; будут центрировать содержимое. Если у вас есть хоть какой-то опыт работы с flex или center, вы знаете, как они могут поломать прокрутку. Использование Overflow:auto на флекс-контейнерах ненадежно и часто не работает вообще. Поэтому вместо того, чтобы устанавливать flex на внешний контейнер, мы устанавливаем его на первый тег div внутри DIV.modal.

У нас есть два a.modalClose. Снабдим их классами, потому что они могут понадобиться в таких местах, где выбирать их с помощью структуры будет означать переусложнение. Первый из них предназначен для полноэкранного закрытия: то есть, если вы кликните за пределами актуальной области содержимого, он закроет модальный режим. Второй нужен для сгенерированного содержимого и добавляет заметную кнопку закрытия: создает четкое визуальное пятно для завершения диалога, а также работает в тех случаях, когда экран слишком мал, чтобы отображалась область вокруг актуального содержимого в <section>.

Оба этих якоря установлены в hidden и aria-hidden=”true”, так что User-Agent не-экранных медиа будут игнорировать оба элемента. Программы для чтения с экрана, программы для чтения шрифта Брайля, поисковые системы и тому подобные юзер-агенты будут делать вид, что этих якорей не существует. Они не будут фокусироваться на них при навигации по клавиатуре и не будут пытаться читать их содержимое. Их не существует ни в одном современном юзер-агенте, если только мы не нацеливаем их на отображение, изменяя их состояние с помощью CSS. Поэтому при использовании media=”screen” в таблице стилей <link> мы можем включить все необходимое только для устройств с экранными медиа.

Примечание: в 99% случаев, когда вы видите <link> или <style>, в которых отсутствует media=””, или установлено значение media=”all”, вы смотрите на досадные три “н” веб-разработки: невежество, некомпетентность и неумелость. Вот почему медиа-запросы, которые содержат “screen and” во внешней таблице стилей  —  такое же невежество. Впрочем, это же в большинстве случаев можно сказать и о <style> или style=””.

Поскольку div семантически нейтральны, а мы скрываем якоря, юзер-агенты не-экранных медиа и поиск фактически будут рассматривать вышеприведенную разметку так:

<h2>Our Modal</h2>
<p>
  A Sample Modal
</p>

И больше ничего. Еще лучше будет альтернативная навигация, которая позволит ссылкам на модальные переходы переходить к этим подразделам без всяких визуальных эффектов, как любая хэш-ссылка на странице. Ссылки даже попадают в историю браузера, поэтому “назад” также закроет модальный диалог и/или приведет пользователей туда, где они были до того, как они нажали на “модальную” ссылку.

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

Это идеальный вариант! Он решает все проблемы доступности, которые могут возникнуть с модальными диалогами на странице.

Помните: h1 (в единственном числе)  —  это заголовок (тоже в единственном числе), для которого всё остальное на сайте  —  подразделы. Как название в верхнем колонтитуле каждой страницы в книге или газете. h2 отмечает начало основных подразделов страницы, h3  —  начало подразделов h2, предшествующих им. h4 отмечает начало подразделов h3 перед ними. Догадаетесь сами, для чего предназначены h5 и h6? Даже скромный hr означает изменение темы или раздела, а не просто “провести линию по экрану”.

Вот почему не стоит просто так перескакивать на пятый уровень глубины или начинать документ с h4.

Иные вопросы

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

Самый простой вариант обойти это  —  дополнительный div в качестве ложного body, который станет двойником модального диалога. Но, как было сказано выше, прокрутка через overflow:auto; ломает flex, поэтому мы добавим дополнительный внутренний div. Таким образом можно добиться от диалога 100% минимальной высоты через флекс.

<div id="fauxBody"><div id="fauxInner">
<!-- здесь содержимое страницы -->
<!-- #fauxInner, #fauxBody --></div></div>

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

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

<header id="top">  <h1>
    Modal Site Demo
  </h1>
  
  <div id="mainMenu">
    <a href="#" class="modalClose" hidden aria-hidden="true"></a>
    <div><nav>
      <a href="#" class="modalClose" hidden aria-hidden="true"></a>
      <ul>
        <li><a href="#submenu">Sub Menu</a></li>
        <li><a href="#search">Search</a></li>
        <li><a href="#login">Log In</a></li>
      </ul>
    </nav></div>
  <!-- #mainMenu --></div>
  
  <a href="#mainMenu" class="mainMenuOpen" hidden aria-hidden="true"></a>
  
 <!-- #top --></header>

Я даю заголовку идентификатор #top в качестве якоря для ссылок “назад к началу”. Внешний div получает идентификатор, чтобы a.mainMenuOpen указывал на него. Мы берем nav вместо section, так что вместо дополнительной div-“обертки” можем просто применять ul. Все связанные с модальным диалогом якоря снабжаются тем же самым hidden и aria-hidden=”true”, так что программы чтения с экрана игнорируют их.

Стиль

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

.modal .modalClose {
 display:inline; /* убирает hidden */
 text-decoration:none;
}

Затем нам нужно разместить все контейнеры, связанные с модальными диалогами, включая #fauxBody.

.modal,
.modal > .modalClose,
#fauxBody {
  position:absolute;
  top:0;
  left:0;
  width:100%;
  height:100%;
  overflow:auto;
}

Обычно вы бы применяли position:fixed, но на самом деле это может вызвать проблемы с прокруткой и позиционированием в Webkit. Поскольку всё это  —  дочерние элементы body, все они с полным размером экрана, а всё содержимое будет размещаться внутри этих контейнеров, ту же функцию может взять на себя position:absolute.

Отсюда займемся модальными диалогами.

.modal {
  left:-100vw;
  opacity:0;
  padding:1em;
  transition:opacity 0.3s, left 0s 0.3s;
  background:radial-gradient(
    hsla(220, 100%, 100%, 0.8) 20%,
    hsla(220, 100%, 85%, 0.95) 100%
  );
}

Поскольку тут уже есть width:100% и overflow:auto, то, чтобы скрыть их, нам нужно только сдвинуть их с левой стороны экрана по видимой ширине. НЕ ИСПОЛЬЗУЙТЕ ДЛЯ ЭТОГО DISPLAY:NONE; ИЛИ VISIBILITY:HIDDEN;! Это помешает поисковым системам, ищущим злоупотребления маскировкой контента, и не позволит им увидеть содержимое этих элементов.

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

.modal:target {
  left:0;
  opacity:1;
  transition:opacity 0.3s, left 0s;
}

И затем отображаем его. Здесь как раз начинается реализация функциональности. Left:0 для показа и постепенное проявление непрозрачности, чтобы было красиво.

С внутренним div:

.modal > div {
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:100%;
}

С помощью флекса отцентрируем section и nav:

.modal > div > section,
.modal > div > nav {
  position:relative;
  overflow:hidden;
  width:100%;
  max-width:24em;
  background:hsl(220, 100%, 95%);
  border:1px solid #0484;
  border-radius:0.5em;
  box-shadow:0 0.25em 1em #0006;
  transform:scale(0);
  transition:transform 0.3s;
}

Делаем любой стиль, который мы хотим. Я использую transform:scale animated так, чтобы, когда модальный диалог является целью target, он “масштабировался” в фокус.

.modal:target > div > section,
.modal:target > div > nav {
  transform:scale(1);
}

Внутренний .modalClose:

section .modalClose,
nav .modalClose {
  position:absolute;
  top:0;
  right:0.325em;
  font-size:1.75em;
  color:#C00;
  transition:transform 0.3s;
}section .modalClose:after,
nav .modalClose:after {
  content:"\1F5D9";
}section .modalClose:focus,
section .modalClose:hover,
nav .modalClose:focus,
nav .modalClose:hover {
  transform:scale(1.2);
}

Получаем сгенерированный контент UTF-8, который затем с помощью абсолютного позиционирования ставим над нашим h2.

А дальше уже можно заняться нормальной стилизацией компонентов.

Главное меню должно находиться внутри медиа-запроса, поэтому нам нужно скопировать большую часть того же кода. Не самый лучший вариант, так как мы его просто удваиваем. Тем не менее как-то иначе обойти это нельзя. (Если бы только “empty” в CSS3 и общий селектор сиблингов работали в сгенерированном содержимом!)

Сначала мы делаем “гамбургер” через сгенерированный контент и границы.

.mainMenuOpen {
    display:block;
    padding:0.25em;
    text-decoration:none;
    border-radius:0.5em;
    transition:transform 0.3s;
  }
  
  .mainMenuOpen:focus,
  .mainMenuOpen:hover {
    transform:scale(1.2);
  }
  
  .mainMenuOpen:before,
  .mainMenuOpen:after {
    content:"";
    display:block;
    width:1.75em;
    height:0.325em;
    border:solid #000A;
    border-width:0.325em 0;
  }
  
  .mainMenuOpen:after {
    border-top:none;
  }

Затем прописываем target для .modalClose, чтобы он стал видимым.

#mainMenu .modalClose {
    display:inline; /* отменить hidden */
    text-decoration:none;
  }

И то же самое для .modal:

#mainMenu,
  #mainMenu > .modalClose {
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
    overflow:auto;
  }

  #mainMenu {
    left:-100vw;
    opacity:0;
    padding:1em;
    transition:opacity 0.3s, left 0s 0.3s;
    background:radial-gradient(
      hsla(220, 100%, 100%, 0.8) 20%,
      hsla(220, 100%, 85%, 0.95) 100%
    );
  }

  #mainMenu:target {
    left:0;
    opacity:1;
    transition:opacity 0.3s, left 0s;
  }

И так далее, и тому подобное. В общем, идея ясна. Еще одна забавность, которую мы можем сделать с меню,  —  вместо заголовка h2 воспользоваться сгенерированным содержимым для добавления ложного заголовка.

#mainMenu nav:before {
    content:"Main Menu";
    display:block;
    padding:0.4em 2.8em 0.4em 0.8em;
    font-size:1.25em;
    background:#0482;
    border-bottom:1px solid #0484;
  }

Обеспечиваем визуальную согласованность.

Наглядная демонстрация

Вы можете посмотреть, как это работает, здесь:

Демо модального сайта без JavaScript.

Как и во всех моих примерах, каталог: https://cutcodedown.com/for_others/medium_articles/modalSite/.

Он открыт для легкого доступа ко всем фрагментам и мелочам. Я разместил там .txt-файл разметки для тех, кто стесняется просмотра исходного кода, а также .rar целиком.

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

Плюсы и минусы

Начнём в первую очередь с проблем.

Недостатки

  1. Множественные открытия-закрытия могут заполнить историю браузера.
  2. Для согласованности необходимо в точности следовать разметке за пределами самого внутреннего div. Это можно счесть “хрупкой разметкой”.
  3. CSS-селекторы могут показаться сложными, особенно если вы считали, что они замедляют рендеринг.
  4. В конечном итоге вы получаете дополнительную разметку вокруг контента. Имейте в виду: если вы делаете макет с минимальной высотой, то, скорее всего, у вас уже есть такая разметка!
  5. Этот метод опирается на функции, которых нет в ряде устаревших браузеров. В частности, могут возникнуть проблемы с IE. В таком случае не отправляйте CSS в IE, оставив чистую разметку.

Преимущества

  1. Использование семантики, hidden и aria-hidden приводит страницу в стопроцентное соответствие минимальным требованиям доступности.
  2. Никакого JavaScript. Это не только убирает проблемы с доступностью, но и приводит к тому, что действия срабатывают более плавно и чисто. В отличие от многих JavaScript “фреймворков”, которые занимаются этим через “вычисления” на основе таймера.
  3. Последовательная структура позволяет легко и просто копировать существующие примеры в новые конструкции.
  4. Избыточные классы сведены к минимуму, что приводит к лучшему использованию моделей кэширования. Помните: не нужно писать в разметке пространных описаний, будь то ваши тэги, идентификаторы или классы. Просто называйте их своими именами. Чем больше презентации вы добавите в разметку  —  даже в виде классов  —  тем медленнее будет страница. Вот почему заявления, будто бы присваивание классов всему подряд, например через BEM, каким-то волшебным образом делает страницу “быстрее” или “лучше”, неверны.
  5. Поскольку всё это управляется CSS, очень легко переключать анимации по вашему усмотрению. Просто играйте с трансформацией, переходами и позиционированием.

Почему стоит отказывать от JavaScript

До сих пор мы изо всех сил старались избегать использования JavaScript, но это не значит, что мы не можем или не должны его применять.

Например, нас может не волновать, как он заполняет историю браузера. Вы можете подключить ко всем якорям .modalClose указания, что если предыдущий хэш пуст, но страница осталась та же, вместо якоря должно срабатывать window.history.back().

Если тогда вы позволите себе JavaScript  —  это нормально. Но только тогда. Пусть всё, что может работать без JavaScript, работает без него. Сначала сосредоточьтесь на нормальной загрузке страниц и нормальной навигации по содержимому. Качественный JavaScript должен улучшать уже работающую страницу, а не оставаться единственным средством обеспечения функциональности.

Если сайт не адаптирован для людей с ограниченными возможностями, то за это можно понести юридическую ответственность. Более того, множество пользователей просто не смогут воспользоваться вашим продуктом. Именно поэтому так важно взять на вооружение правильную семантику и логическую структуру, а также учитывать, что происходит с изображениями, скриптами и даже CSS. Мы должны помнить, что CSS имеет медиа-цели (экран, печать, речь и т. д.), и если вы не прописываете их, когда используете <link> или <style> или отправляете “всем”, то они могут создавать проблемы с доступностью  —  или просто зря тратить пропускную способность.

Скорость также играет роль. Не стоит применять гигантский JS-фреймворк с тысячами строк индивидуальных скриптов, чтобы выполнить то, с чем могут справиться четыре-шесть тысяч строк CSS. Даже некоторые клиенты начинают сосредотачиваться на таких параметрах, как “значимое время отображения” в инструментах вроде Lighthouse, а раздутые медленные скрипты и огромные “фреймворки” могут поставить на этом крест.

Как веб-профессионалы, мы просто должны начать думать в этом направлении.

Вывод

Избегая JavaScript в пользу CSS3, можно создавать страницы, доступные для пользователей с ограниченными возможностями, и дополнять их модными экранными эффектами. Благодаря современным функциям браузера гораздо проще и полезнее сначала написать семантику, добавить несколько div, настроить таргетинг таблицы стилей для экранных медиа, а затем использовать селекторы и псевдо-классы для запуска поведения, а не тратить время на написание скриптов для обработки всего этого.

И еще вот что: разделение интересов. Как бы странно это ни звучало, но если вы взглянете на семантическую навигацию без экранного CSS, то модальные диалоги, по крайней мере реализованные таким образом, нельзя будет назвать поведением. Это представление. А представления  —  это то, чем и занимается CSS!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Jason Knight: Modal Dialog Driven Websites Without JavaScript

Предыдущая статьяРазвенчание мифов о разработке программного обеспечения
Следующая статьяАлгоритм YOLO простым языком