Анимация details: два подхода на CSS и JavaScript

С помощью элемента details в HTML удобно реализовать раскрывающиеся блоки, такие как FAQ или аккордеоны. К сожалению его стандартное поведение не включает анимацию: контент резко появляется и исчезает. В этой статье мы используем два способа для того, чтобы добавить анимацию к details: с помощью псевдоэлемента ::details-content и с использованием  JavaScript (Web Animations API). В обоих случаях это будет раздел FAQ в виде аккордеона.

анимация details

Подход 1: Анимация details с помощью ::details-content

Анимация дает ощущение интерактивности и делает интерфейс приятным для пользователя. Плавное появление контента помогает визуально связать действия пользователя с изменениями на странице. Вот только <details> использует Shadow DOM, что усложняет задачу из-за внутренней логики браузера.

Псевдоэлемент ::details-content управляет содержимым details, то есть любым контентом, который идет после summary. В браузерах на основе WebKit (Chrome, Opera, Edge) к нему можно обратиться также через префикс ::-webkit-details-content. Так мы можем напрямую анимировать высоту или другие свойства содержимого.

Пример:

Что такое элемент details в HTML?
Элемент details — это нативный способ в HTML для создания раскрывающихся блоков, таких как FAQ или аккордеоны, без JavaScript.
Когда появился элемент details?
Элемент details был введен в HTML5 и впервые реализован в Chrome 12 в 2011 году, позже добавлен в другие браузеры.
Зачем использовать details в проектах?
Он обеспечивает семантику, доступность и встроенное поведение аккордеона, упрощая разработку интерактивных интерфейсов.
Как анимация details улучшает доступность?
Благодаря поддержке ARIA и клавиатурной навигации, details упрощает взаимодействие для пользователей с ограничениями.

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

Преимущества ::details-content

Чистый CSS: не требует js код, что упрощает код и снижает нагрузку.

Простота: достаточно нескольких строк стилей для базовой анимации.

Нативность: сохраняется семантическое поведение details.

Недостатки: работает только в WebKit-браузерах. Firefox не предоставляет доступ к Shadow DOM через псевдоэлементы, а значит, анимация в нем невозможна.

Подход 2: Использование для анимации JavaScript (WAAPI)

Web Animations API позволяет управлять анимацией, напрямую изменяя свойства элемента, в данном случае нам важна анимация высоты контента. Мы перехватываем клик на summary, отключаем стандартное поведение и анимируем высоту от нуля до высоты содержимого (или наоборот при закрытии).

Пример:

Lorem ipsum dolor sit amet.
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Earum atque, sequi laudantium explicabo cupiditate eaque?
Soluta praesentium cumque veritatis error?
Nam culpa dolorum nesciunt dolore! Sequi at quaerat beatae quas ullam a doloremque delectus culpa?
Voluptate laboriosam ex blanditiis mollitia!
Atque a porro incidunt cum veritatis nihil quo, quasi facere pariatur magnam similique voluptatum! A.
Odit deserunt alias illo vero?
Ipsa amet cum iure, aut repellat laudantium. Eaque aliquam neque temporibus similique deserunt voluptatibus facere.

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>

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

Кроссбраузерность: код будет работать во всех современных браузерах, включая Firefox, Chrome и Safari.

Контроль анимации details: управление длительностью, плавностью анимации (easing).

Стабильность: Обходит ограничения Shadow DOM, так как анимация применяется к самому details.

Недостатки: дополнительные код javascript.

Сравнение подходов

Критерий::details-contentWAAPI
Поддержка браузеровТолько WebKitВсе современные браузеры
Использование JSНе требуетсяТребуется
Сложность реализацииНизкаяСредняя
ГибкостьОграниченнаяВысокая
СемантикаПолностью сохраняетсяСохраняется с оговорками

Заключение

Комбинируйте с name: атрибут name в details позволяет нативно реализовать поведение аккордеона (только один элемент открыт). Это полезно для обоих подходов, и если нужно минимализировать JavaScript.

Тестируйте в Firefox: если вы выбрали вариант с ::details-content, проверьте, как страница выглядит в Firefox без анимации — содержимое details должно появляться без багов.

По теме:

Блок вопросов и ответов, FAQ на сайте

Emmet в VS Code для HTML

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *