С помощью элемента details в HTML удобно реализовать раскрывающиеся блоки, такие как FAQ или аккордеоны. К сожалению его стандартное поведение не включает анимацию: контент резко появляется и исчезает. В этой статье мы используем два способа для того, чтобы добавить анимацию к details: с помощью псевдоэлемента ::details-content и с использованием JavaScript (Web Animations API). В обоих случаях это будет раздел FAQ в виде аккордеона.
Анимация дает ощущение интерактивности и делает интерфейс приятным для пользователя. Плавное появление контента помогает визуально связать действия пользователя с изменениями на странице. Вот только <details> использует Shadow DOM, что усложняет задачу из-за внутренней логики браузера.
Псевдоэлемент ::details-content управляет содержимым details, то есть любым контентом, который идет после summary. В браузерах на основе WebKit (Chrome, Opera, Edge) к нему можно обратиться также через префикс ::-webkit-details-content. Так мы можем напрямую анимировать высоту или другие свойства содержимого.
Пример:
CSS и HTML:
<style>
details {
position: relative;
border-radius: 4px;
border:1px solid rgba(0,0,0,.1);
margin-bottom: 2px;
overflow: hidden;
}
summary {
padding:15px;
position: relative;
font-weight: 700;
cursor: pointer;
}
details::details-content {
display: block;
block-size: 0;
overflow: hidden;
transition: all .5s allow-discrete;
}
details[open]::details-content {
block-size: auto;
block-size: calc-size(auto, size);
}
.details__content {
padding: 0 15px;
display:grid !important;
grid-template-rows: 0fr;
transition: all .3s ease;
}
details[open] .details__content {
padding: 15px;
grid-template-rows: 1fr;
}
summary::marker {
content: none;
}
summary::after {
content: '\203A';
position: absolute;
transform:rotate(0);
right: 15px;
top:15px;
transform-origin: center;
transition: transform 0.3s ease;
}
details[open] summary::after {
transform:rotate(90deg);
}
.details__content{
padding:0 15px;
}
details[open] .details__content{
padding:15px;
}
</style>
<!-- HTML (Emmet) -->
(details>(summary[name="faq"]>lorem5)+(.details__content>lorem15))*4
Чистый CSS: не требует js код, что упрощает код и снижает нагрузку.
Простота: достаточно нескольких строк стилей для базовой анимации.
Нативность: сохраняется семантическое поведение details.
Недостатки: работает только в WebKit-браузерах. Firefox не предоставляет доступ к Shadow DOM через псевдоэлементы, а значит, анимация в нем невозможна.
Web Animations API позволяет управлять анимацией, напрямую изменяя свойства элемента, в данном случае нам важна анимация высоты контента. Мы перехватываем клик на summary, отключаем стандартное поведение и анимируем высоту от нуля до высоты содержимого (или наоборот при закрытии).
Пример:
CSS:
<style>
.faq__js {
width: 600px;
max-width:100%;
}
.faq__js details {
position: relative;
border-radius: 4px;
border: 1px solid rgba(0,0,0,.1);
margin-bottom: 2px;
overflow: hidden;
}
.faq__js summary {
padding: 15px;
position: relative;
font-weight: 700;
cursor: pointer;
background: #fff;
transition: .3s;
outline: none;
border: 1px solid #eee;
}
.faq__js summary:focus {
border-color: #333;
}
.faq__js .details__content {
padding: 15px;
border-top: none;
}
.faq__js summary::marker {
content: none;
}
.faq__js summary::after {
content: '\203A';
position: absolute;
right: 15px;
top: 50%;
transform: rotate(0deg);
transform-origin: center;
transition: transform 0.3s ease;
margin-top: -0.5em;
}
.faq__js details[open] summary::after {
transform: rotate(90deg);
}
</style>
HTML + JS:
<!-- HTML (Emmet) -->
.faq__js>(details[name="faq_item"]>(summary>lorem5)+(.details__content>lorem15))*4
<script>
class Accordion {
constructor(el) {
this.el = el;
this.summary = el.querySelector('summary');
this.content = el.querySelector('.details__content');
this.animation = null;
this.isClosing = false;
this.isExpanding = false;
this.summary.addEventListener('click', (e) => this.onClick(e));
}
onClick(e) {
e.preventDefault();
this.el.style.overflow = 'hidden';
if (this.isClosing || !this.el.open) {
const others = Array.from(document.querySelectorAll('.faq__js details')).filter(other => other !== this.el && other.open);
if (others.length > 0) {
const closePromises = others.map(other => {
return new Promise(resolve => {
other.accordion.shrink(resolve);
});
});
Promise.all(closePromises).then(() => {
this.open();
});
} else {
this.open();
}
} else if (this.isExpanding || this.el.open) {
this.shrink();
}
}
shrink(callback = () => {}) {
this.isClosing = true;
const startHeight = `${this.el.offsetHeight}px`;
const endHeight = `${this.summary.offsetHeight}px`;
if (this.animation) {
this.animation.cancel();
}
this.animation = this.el.animate({
height: [startHeight, endHeight]
}, {
duration: 400,
easing: 'ease-out'
});
this.animation.onfinish = () => {
this.el.open = false;
this.el.style.height = '';
this.el.style.overflow = '';
this.animation = null;
this.isClosing = false;
callback();
};
this.animation.oncancel = () => this.isClosing = false;
}
open() {
this.el.style.height = `${this.el.offsetHeight}px`;
this.el.open = true;
window.requestAnimationFrame(() => this.expand());
}
expand() {
this.isExpanding = true;
const startHeight = `${this.el.offsetHeight}px`;
const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;
if (this.animation) {
this.animation.cancel();
}
this.animation = this.el.animate({
height: [startHeight, endHeight]
}, {
duration: 400,
easing: 'ease-out'
});
this.animation.onfinish = () => {
this.el.style.height = '';
this.el.style.overflow = '';
this.animation = null;
this.isExpanding = false;
};
this.animation.oncancel = () => this.isExpanding = false;
}
}
document.querySelectorAll('.faq__js details').forEach((el) => {
el.accordion = new Accordion(el);
});
</script>
Кроссбраузерность: код будет работать во всех современных браузерах, включая Firefox, Chrome и Safari.
Контроль анимации details: управление длительностью, плавностью анимации (easing).
Стабильность: Обходит ограничения Shadow DOM, так как анимация применяется к самому details.
Недостатки: дополнительные код javascript.
Критерий | ::details-content | WAAPI |
---|---|---|
Поддержка браузеров | Только WebKit | Все современные браузеры |
Использование JS | Не требуется | Требуется |
Сложность реализации | Низкая | Средняя |
Гибкость | Ограниченная | Высокая |
Семантика | Полностью сохраняется | Сохраняется с оговорками |
Комбинируйте с name: атрибут name в details позволяет нативно реализовать поведение аккордеона (только один элемент открыт). Это полезно для обоих подходов, и если нужно минимализировать JavaScript.
Тестируйте в Firefox: если вы выбрали вариант с ::details-content, проверьте, как страница выглядит в Firefox без анимации — содержимое details должно появляться без багов.
Блок вопросов и ответов, FAQ на сайте
Добавить комментарий