Плавные спойлеры (HTML5 & CSS3)

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

Принцип работы тега «details»

Элемент <details> представляет собой одно из многочисленных семантических нововведений HTML5 и предназначается для хранения контента, отображаемого или скрываемого по желанию пользователя. В отличие от традиционных методов разработки спойлера через <input type="checkbox" /> и псевдокласса :checked или посредством id элемента и псевдокласса :target, для корректной работы элемента <details> не требуется добавление стороннего HTML и CSS кода, что делает его использование простым и удобным.

Содержимое тега становится видимым, когда у него появляется атрибут open, который автоматически добавляется или удаляется браузером при клике по <summary> — заголовку спойлера. В свою очередь, заголовок должен стоять первым среди дочерних элементов, но его наличие не является обязательным. В случае отсутствия заголовка браузер установит стандартное название «подробности» из Shadow DOM, однако такой тег не пройдет валидацию. Это аналогично ситуации, когда <section> не имеет <h1—h5>.

<details> <!-- <details open> -->
  <summary>Название</summary>
  <p>Открываемый по требованию контент</p>
</details>

Элемент <details> может содержать после своего заголовка блочный или строчный контент, принадлежащий к категории основной поточной информации (flow content). Содержимое, указанное после <summary>, как могло бы показаться, не будет отображаться и скрываться средствами CSS, а именно — переключением значения display. Браузер переносит его в необходимое место за счёт манипуляций с Shadow DOM, что делает возможным применить к контенту эффекты перехода (transition). Иными словами, в примере выше при отсутствии у спойлера атрибута open, для элемента <p> правило display: none не применяется.

Не смотря на кажущуюся практичность и тривиальность, элемент <details> не лишён недостатков: отсутствие полной кроссбраузерной поддержки (речь идет об ограничениях в Internet Explorer, Edge и игнорирование Opera Mini) и непрезентабельное стандартное со (простой маркер перед заголовком <summary>, отсутствие анимации), поэтому при создании спойлеров необходимо учесть эти аспекты.

Как создать спойлеры

Прежде чем приступить к разметке кода и написанию CSS, важно обозначить три принципа, которым должно соответствовать красивое оформление спойлера:

  1. Спойлеру необходимо выглядеть визуально интерактивным. Пользователь должен без особого труда понимать, что элемент является кликабельным;
  2. Для визуально привлекательного плавного отображения и скрытия контента к спойлеру следует добавить эффекты перехода;
  3. Открытый спойлер должен каким бы то ни было образом выделяться, чтобы сфокусировать внимание читателя на себе среди прочих неактивных, если на странице расположены несколько спойлеров подряд.

HTML-разметка

Предположим, необходимо создать такую популярную секцию для сайта, как небольшой раздел FAQ, где ответ появлялся бы после клика на сам вопрос. Для этого потребуется следующий незамысловатый HTML:

<section>
  <h2>Frequently asked questions</h2>
  <details>
    <summary tabindex="1"><span>Question #1</span></summary>
    <p>Answer #1</p>
  </details>
  <details>
    <summary tabindex="2"><span>Question #2</span></summary>
    <p>Answer #2</p>
  </details>
  <details>
    <summary tabindex="3"><span>Question #3</span></summary>
    <p>Answer #3</p>
  </details>
<section>

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

Изменение стандартного маркера

Основную сложность при добавлении CSS может вызвать изменение стиля для маркера, стоящего перед заголовком спойлера. В браузерах на движке WebKit для стилизации маркеров <summary> существует специальный псевдоэлемент ::-webkit-details-marker, в то время как Firefox отображает этот элемент как часть списка, то есть с правилом display: list-item, что, соответственно, позволяет удалить маркер простой сменой свойства display или же через псевдоэлемент ::-moz-list-bullet:

summary {
  &::-webkit-details-marker { display: none; }
  &::-moz-list-bullet { list-style-type: none; }
  // display: block; - не всегда уместно
}

Использование display: block в данном случае приведет к нежелательному последствию: кликабельная область содержимого заголовка растянется на 100% ширины родительского элемента, а в идеале она должна соответствовать ширине собственного контента, поэтому в зависимости от необходимости рекомендуется прибегнуть к значениям display, например, inline, inline-block inline-flex, table или оставить заголовок как блочный и установить ширину содержимого как max-content или fit-content, но далеко не все браузеры поддерживают такие значения для width.

Добавление маркера и эффектов перехода

Когда стандартный маркер удалён, к спойлеру может быть добавлено собственное изображение через псеводоэлемент ::before. В готовом примере для этого используется SVG-иконка из коллекции Google Material Icons, которая выводится через background-image в base64, что позволяет избежать лишних подключений к внешним ресурсам.

Особую трудность при стилизации спойлеров представляют эффекты перехода, которые должны срабатывать как при открытии, так и при закрытии контента. Для их реализации следует использовать свойство transition, в основе которого будет лежать изменяющаяся минимальная и максимальная высота спойлера, т. е. самого элемента <detail> и прозрачность — для всего контента, стоящего после <summary>:

// Иконка маркера в base64
$img-marker: 'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjM0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIzNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNOC41OSAxNi4zNGw0LjU4LTQuNTktNC41OC00LjU5TDEwIDUuNzVsNiA2LTYgNnoiLz48L3N2Zz4=';

details {

  // Косметические улучшения
  position: relative;
  margin-bottom: .5rem;
  min-height: 1rem;
  max-height: 3rem;
  transition: min-height .15s linear, max-height .5s linear;
  -webkit-transition: min-height .15s linear, max-height .15s linear;
  overflow: hidden;

  // Заголовок
  summary {

    // Удаление стандартного маркера
    &::-webkit-details-marker { display: none; }
    &::-moz-list-bullet { list-style-type: none; }

    // Ограничение кликабельной области заголовка
    display: inline-block;

    // Пространство для маркера
    padding-left: 1.5em;

    // Косметические улучшения
    cursor: pointer;
    outline: 0;
    transition: color .12s;
    -webkit-transition: color .12s;
    span { border-bottom: 1px currentColor dotted; }
    &:hover { color: #d06c6c; }

    // Добавление маркера
    &::before {
      content: "";
      left: 0;
      top: .4em;
      position: absolute;
      background: url($img-marker) no-repeat 50% 50% / 1em 1em;
      width: 1em;
      height: 1em;
      transition: transform .1s linear;
      -webkit-transition: transform .1s linear;
    }

    // Контент, стоящий после заголовка
    & ~ * {
      padding-left: 1.5em;
      opacity: 0;
      transition: opacity .15s linear;
      -webkit-transition: opacity .15s linear;
    }

  }

  // Открытый спойлер
  &[open] {
    min-height: 2em;
    max-height: 20em;
    summary {
      color: #d06c6c;

      & ~ * { 
        opacity: 1;
      }
      &:before { 
        transform: rotate(90deg);
        -webkit-transform: rotate(90deg);
        -moz-transform: rotate(90deg);
      }

    }

  }

}

Решение проблем кроссбраузерности

Для обеспечения правильной работы спойлеров в Internet Explorer 10+ и Edge силами одного лишь CSS предлагается совместить <summary> и псевдокласс :focus таким образом, чтобы содержимое спойлера появлялось при появлении фокуса на заголовке. Чтобы применить указанный псевдокласс к заголовкам, в HTML-разметке они должны содержать упомянутый ранее атрибут tabindex. Кроме того, браузеры от Microsoft не знают тега <details> и не добавляют к нему атрибут open при клике, поэтому часть CSS придется дублировать. Весь полученный код скрывается от прочих браузеров:

// Фикс для IE 10-11 & Edge
@mixin ie_fix() {
  details {
    max-height: none;
    summary {

      & ~ * {
        max-height: 0;
        overflow: hidden;
        position: absolute; // IE испытывает проблемы с обычным max-height: 0;
      }

      &:focus {
        color: #d06c6c;

        &::before { 
          transform: rotate(90deg);
          -ms-transform: rotate(90deg);
         }

        & ~ * {
          max-height: 20em;
          position: static;
          opacity: 1;
        }

      }

    }
  }
}

// IE 10-11
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
  @include ie_fix();
}

// Edge
@supports (-ms-ime-align:auto) {
  @include ie_fix();
}

Этот фикс содержит небольшое функциональное ограничение: на странице нельзя будет открыть одновременно несколько спойлеров, т.к. фокус действует только на один элемент. Кроме того, в результате подобной реализации теряется плавный переход при открытии спойлера, но остается работать эффект прозрачности. При необходимости посредством CSS-хака миксин ie_fix() может быть добавлен и для старой Opera 12 (Presto).

Пример CSS-спойлеров

В результате объединения всего SCSS и добавления соответствующей HTML-разметки получаются простые кроссбраузерные спойлеры без использования JS.

В заключение

Не смотря на преимущество с точки зрения семантики и практичности использования, тег <details> не рекомендуется применять там, где требуется поддержка «древних» Internet Explorer и устаревших мобильных браузеров. В этом случае стоит прибегнуть к традиционным способам создания спойлеров на CSS, работающих практически везде, или использовать решения на JavaScript, в том числе HTML5-полифиллы. Остается надеется, что со временем старые браузеры окончательно уйдут в небытие и уступят место их обновленным версиям, и тогда новые семантические теги станут повсеместной практикой.