I'm trying to build expand/collapse section animations using this guide from Google for best performance. The article mentions in the end
A word of warning on this particular variant: Chrome has blurry text on low DPI screens during the animation because of rounding errors due to the scale and counter-scale of the text. If you’re interested in the details for that, there’s a bug filed that you can star and follow.
Tried manually fixing the frames with rounding errors, but no luck.
Screenshot from Firefox during animation (text is not smooth):
Screenshot from Firefox after animation ends (flashes to smooth text):
Some observations
will-change
from the animated elements, but obviously doesn't do the trick for FFox).will-change
, otherwise will flash to smooth.Since code is longer I provided also Codepen for your convenience, also embedded here.
"use strict";
class Expando {
constructor() {
this._el = document.querySelector(".content");
const toggleEl = document.querySelectorAll(".section-header");
this._toggleBtn = document.querySelector(".toggle");
this._sections = [...document.querySelectorAll(".expand-collapse-section")];
this._isExpanded = new Array(this._sections.length).fill(false);
this._createEaseAnimations();
toggleEl.forEach((el) => {
el.addEventListener("click", (e) => {
el.querySelector(".toggle").classList.toggle("expanded");
const section = e.target.closest(".expand-collapse-section");
const content = section.querySelector(".content");
const idx = this._sections.indexOf(section);
this.toggle(content, idx);
});
});
// measure single content element's margin-top (they all have the same margin in CSS)
const rgx = /(.+)px/;
const marginTopPx = window.getComputedStyle(this._el, null).marginTop;
const results = rgx.exec(marginTopPx);
this._contentTopMargin = +results[1] || 0;
}
expand(el, sectionIdx) {
if (this._isExpanded[sectionIdx]) {
return;
}
this._isExpanded[sectionIdx] = true;
this._applyAnimation(el, { expand: true });
}
collapse(el, sectionIdx) {
if (!this._isExpanded[sectionIdx]) {
return;
}
this._isExpanded[sectionIdx] = false;
this._applyAnimation(el, { expand: false });
}
toggle(el, sectionIdx) {
const expanded = this._isExpanded[sectionIdx];
if (expanded) {
return this.collapse(el, sectionIdx);
}
this.expand(el, sectionIdx);
}
_applyAnimation(el, { expand } = opts) {
function setTranslation(el, { height, start, expand } = opts) {
const translation = start ? (expand ? -height : 0) : (expand ? 0 : -height);
if (translation === 0) {
el.removeAttribute("style");
} else {
el.style.transform = `translateY(${translation}px)`;
}
}
const elInner = el.querySelector(".content-inner");
el.classList.remove("item--expanded");
el.classList.remove("item--collapsed");
elInner.classList.remove("item__contents--expanded");
elInner.classList.remove("item__contents--collapsed");
const sectionEl = el.closest(".expand-collapse-section");
const sectionContent = sectionEl.querySelector(".content");
sectionContent.style.display = "block"; // block to expand, has no effect on collapse (in the end of animation it gets set to none)
const index = this._sections.indexOf(sectionEl);
const targetContentHeight = sectionContent.offsetHeight + this._contentTopMargin;
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// don't animate yet translation of adjacent sections, just set initial value for animation
curr.classList.add("notransition");
// setting section content to display block pushes the other items by its height as it has transform set, but it still occupies its original height
// initial value for animation
setTranslation(curr, { height: targetContentHeight, start: true, expand });
}
// the rest of the content below the expandable sections
const lastSectionSibling = this._sections.slice(-1)[0].nextElementSibling;
lastSectionSibling.classList.add("notransition");
setTranslation(lastSectionSibling, { height: targetContentHeight, start: true, expand });
requestAnimationFrame(() => {
if (expand) {
el.classList.add("item--expanded");
elInner.classList.add("item__contents--expanded");
} else {
el.classList.add("item--collapsed");
elInner.classList.add("item__contents--collapsed");
}
sectionEl.offsetHeight; // needed for Firefox on expand
// sectionEl.offsetHeight; -> not needed in requestAnimationFrame
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// trigger translation animation of adjacent sections and rest of the content now
curr.classList.remove("notransition");
setTranslation(curr, { height: targetContentHeight, start: false, expand });
sectionEl.offsetHeight; // needed for Firefox on expand
}
lastSectionSibling.classList.remove("notransition");
setTranslation(lastSectionSibling, { height: targetContentHeight, start: false, expand });
if (!expand) {
sectionContent.addEventListener("animationend", () => {
sectionContent.style.display = "none";
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// avoid unexpected animations when removing transform inline style in the end of the animation, needs reflow
curr.classList.add("notransition");
// could also be set to translateY(0)
curr.removeAttribute("style");
// should force reflow here otherwise there will be no net change in notransition class which would animate transform, which we don't want,
// we're just removing the unnecessary style attribute
sectionEl.offsetHeight;
curr.classList.remove("notransition");
}
lastSectionSibling.classList.add("notransition");
lastSectionSibling.removeAttribute("style");
sectionEl.offsetHeight;
lastSectionSibling.classList.remove("notransition");
}, { once: true });
}
});
}
_createEaseAnimations() {
let ease = document.querySelector(".ease");
if (ease) {
return ease;
}
ease = document.createElement("style");
ease.classList.add("ease");
console.log('------------- Expand animation --------------');
const expandAnimation = [];
const expandContentsAnimation = [];
const collapseAnimation = [];
const collapseContentsAnimation = [];
for (let i = 0; i <= 100; i++) {
const step = this._ease(i / 100);
// Expand animation.
this._append({
i,
step,
start: 0,
end: 1,
outerAnimation: expandAnimation,
innerAnimation: expandContentsAnimation,
});
// Collapse animation.
this._append({
i,
step,
start: 1,
end: 0,
outerAnimation: collapseAnimation,
innerAnimation: collapseContentsAnimation,
});
}
ease.textContent = `
@keyframes expandAnimation {
${expandAnimation.join("")}
}
@keyframes expandContentsAnimation {
${expandContentsAnimation.join("")}
}
@keyframes collapseAnimation {
${collapseAnimation.join("")}
}
@keyframes collapseContentsAnimation {
${collapseContentsAnimation.join("")}
}`;
document.head.appendChild(ease);
return ease;
}
_append({ i, step, start, end, outerAnimation, innerAnimation } = opts) {
let scale = start + (end - start) * step;
let invScale = scale === 0 ? 0 : 1 / scale;
if (start === 0) {
if (i === 11) {
scale = 0.373;
invScale = 2.680965147453083;
}
if (i === 23) {
scale = 0.648;
invScale = 1.5432098765432098;
}
if (i === 28) {
scale = 0.7312;
invScale = 1.3676148796498906;
}
if (i === 41) {
scale = 0.879;
invScale =1.1376564277588168;
}
if (i === 43) {
scale = 0.894;
invScale = 1.1185682326621924;
}
if (i === 44) {
scale = 0.9;
invScale = 1.1111111111111112;
}
if (i === 55) {
scale = 0.959;
invScale = 1.0427528675703859;
}
if (i === 56) {
scale = 0.96;
invScale = 1.0416666666666667;
}
if (i === 62) {
scale = 0.97914;
invScale = 1.0213044099924424;
}
if (i === 64) {
scale = 0.983;
invScale = 1.017293997965412;
}
if (i === 67) {
scale = 0.988;
invScale = 1.0121457489878543;
}
if (i === 69) {
scale = 0.9907648;
invScale = 1.0093212839212697;
}
if (i === 72) {
scale = 0.99385;
invScale = 1.0061880565477688;
}
if (i === 74) {
scale = 0.99543;
invScale = 1.0045909807821745;
}
if (i === 85) {
scale = 0.99949;
invScale = 1.0005102602327187;
}
if (i === 89) {
scale = 0.9998536;
invScale = 1.0001464214360982;
}
if (i === 90) {
scale = 0.99995;
invScale = 1.000050002500125;
}
console.log(`${i}: scale: ${scale}, inverse: ${invScale}, scale * inverse = ${scale * invScale}`);
}
outerAnimation.push(`
${i}% {
transform: scaleY(${scale});
}`);
innerAnimation.push(`
${i}% {
transform: scaleY(${invScale});
}`);
}
_clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
_ease(v, pow = 4) {
v = this._clamp(v, 0, 1);
return 1 - Math.pow(1 - v, pow);
}
}
new Expando();
* {
box-sizing: border-box;
}
html,
body {
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
.expand-collapse-section,
.example-content-below-sections {
transition: transform .7s ease-out;
will-change: transform;
}
.section-header {
border: 1px solid #efefef;
border-radius: 4px;
padding: 0.5em;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-wrapper {
width: 36px;
height: 36px;
background: #f7f8f9;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.toggle {
background-image: url('data:image/svg+xml,<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23747878" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></svg>');
display: inline-block;
height: 24px;
width: 24px;
transition: transform 200ms;
}
.toggle.expanded {
transform: rotateZ(180deg);
}
.content {
transform: scaleY(0);
transform-origin: top left;
overflow: hidden;
will-change: transform;
margin-top: 0.5rem;
border: 1px solid black;
border-radius: 5px;
/* padding: 1em; */
display: none;
overflow: hidden;
}
.content-inner {
transform-origin: top left;
overflow: hidden;
transform: scaleY(0);
will-change: transform;
}
/* need to be margin as with padding, transform makes it push to the top while animating collapse */
.content-inner-spacer {
margin: 1em;
}
.item--expanded {
animation-name: expandAnimation;
animation-duration: 5.7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item__contents--expanded {
animation-name: expandContentsAnimation;
animation-duration: 5.7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item--collapsed {
animation-name: collapseAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item__contents--collapsed {
animation-name: collapseContentsAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,minimum-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
<script src="script.js" defer></script>
</head>
<body>
<section class="expand-collapse-section first">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Neque,
reiciendis quasi laboriosam delectus accusantium aliquam! Alias
voluptatum, dolore debitis aliquam maxime doloribus eligendi tempora
amet laborum quis maiores reprehenderit deleniti impedit quo quas
eius, fugiat atque non accusamus eum esse? Explicabo quam ea
reprehenderit minus officiis vel et reiciendis ex omnis expedita, ab
libero veritatis! Suscipit, magni maxime deserunt eaque laborum libero
atque nesciunt labore consequatur provident. Aut dolor necessitatibus
sint, dicta facilis sed molestiae laudantium incidunt repellat
consequuntur, officiis maxime quam, dolorum possimus expedita minus.
Error quaerat, esse magni quibusdam quis corporis, et tenetur, ullam
ipsam a ratione fugit.
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<p class="example-content-below-sections">Bottom content</p>
</body>
</html>
How can I overcome this issue? I see on Google's search result suggestions expand/collapse sections (scroll a bit to People also ask), they use transforms to animate, but don't have issues.
I ended up using perspective: 1px
on the element I'm animating. Although there's a slight text blur during animation, this for me is a viable solution because:
perspective
perspective
).I'll be accepting my own answer, but I'm open to even better solutions. Here's also the original example with the fix applied (one line change for .content
in the CSS).
"use strict";
class Expando {
constructor() {
this._el = document.querySelector(".content");
const toggleEl = document.querySelectorAll(".section-header");
this._toggleBtn = document.querySelector(".toggle");
this._sections = [...document.querySelectorAll(".expand-collapse-section")];
this._isExpanded = new Array(this._sections.length).fill(false);
this._createEaseAnimations();
toggleEl.forEach((el) => {
el.addEventListener("click", (e) => {
el.querySelector(".toggle").classList.toggle("expanded");
const section = e.target.closest(".expand-collapse-section");
const content = section.querySelector(".content");
const idx = this._sections.indexOf(section);
this.toggle(content, idx);
});
});
// measure single content element's margin-top (they all have the same margin in CSS)
const rgx = /(.+)px/;
const marginTopPx = window.getComputedStyle(this._el, null).marginTop;
const results = rgx.exec(marginTopPx);
this._contentTopMargin = +results[1] || 0;
}
expand(el, sectionIdx) {
if (this._isExpanded[sectionIdx]) {
return;
}
this._isExpanded[sectionIdx] = true;
this._applyAnimation(el, { expand: true });
}
collapse(el, sectionIdx) {
if (!this._isExpanded[sectionIdx]) {
return;
}
this._isExpanded[sectionIdx] = false;
this._applyAnimation(el, { expand: false });
}
toggle(el, sectionIdx) {
const expanded = this._isExpanded[sectionIdx];
if (expanded) {
return this.collapse(el, sectionIdx);
}
this.expand(el, sectionIdx);
}
_applyAnimation(el, { expand } = opts) {
function setTranslation(el, { height, start, expand } = opts) {
const translation = start ? (expand ? -height : 0) : (expand ? 0 : -height);
if (translation === 0) {
el.removeAttribute("style");
} else {
el.style.transform = `translateY(${translation}px)`;
}
}
const elInner = el.querySelector(".content-inner");
el.classList.remove("item--expanded");
el.classList.remove("item--collapsed");
elInner.classList.remove("item__contents--expanded");
elInner.classList.remove("item__contents--collapsed");
const sectionEl = el.closest(".expand-collapse-section");
const sectionContent = sectionEl.querySelector(".content");
sectionContent.style.display = "block"; // block to expand, has no effect on collapse (in the end of animation it gets set to none)
const index = this._sections.indexOf(sectionEl);
const targetContentHeight = sectionContent.offsetHeight + this._contentTopMargin;
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// don't animate yet translation of adjacent sections, just set initial value for animation
curr.classList.add("notransition");
// setting section content to display block pushes the other items by its height as it has transform set, but it still occupies its original height
// initial value for animation
setTranslation(curr, { height: targetContentHeight, start: true, expand });
}
// the rest of the content below the expandable sections
const lastSectionSibling = this._sections.slice(-1)[0].nextElementSibling;
lastSectionSibling.classList.add("notransition");
setTranslation(lastSectionSibling, { height: targetContentHeight, start: true, expand });
requestAnimationFrame(() => {
if (expand) {
el.classList.add("item--expanded");
elInner.classList.add("item__contents--expanded");
} else {
el.classList.add("item--collapsed");
elInner.classList.add("item__contents--collapsed");
}
sectionEl.offsetHeight; // needed for Firefox on expand
// sectionEl.offsetHeight; -> not needed in requestAnimationFrame
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// trigger translation animation of adjacent sections and rest of the content now
curr.classList.remove("notransition");
setTranslation(curr, { height: targetContentHeight, start: false, expand });
sectionEl.offsetHeight; // needed for Firefox on expand
}
lastSectionSibling.classList.remove("notransition");
setTranslation(lastSectionSibling, { height: targetContentHeight, start: false, expand });
if (!expand) {
sectionContent.addEventListener("animationend", () => {
sectionContent.style.display = "none";
for (let i = index + 1; i < this._sections.length; i++) {
const curr = this._sections[i];
// avoid unexpected animations when removing transform inline style in the end of the animation, needs reflow
curr.classList.add("notransition");
// could also be set to translateY(0)
curr.removeAttribute("style");
// should force reflow here otherwise there will be no net change in notransition class which would animate transform, which we don't want,
// we're just removing the unnecessary style attribute
sectionEl.offsetHeight;
curr.classList.remove("notransition");
}
lastSectionSibling.classList.add("notransition");
lastSectionSibling.removeAttribute("style");
sectionEl.offsetHeight;
lastSectionSibling.classList.remove("notransition");
}, { once: true });
}
});
}
_createEaseAnimations() {
let ease = document.querySelector(".ease");
if (ease) {
return ease;
}
ease = document.createElement("style");
ease.classList.add("ease");
console.log('------------- Expand animation --------------');
const expandAnimation = [];
const expandContentsAnimation = [];
const collapseAnimation = [];
const collapseContentsAnimation = [];
for (let i = 0; i <= 100; i++) {
const step = this._ease(i / 100);
// Expand animation.
this._append({
i,
step,
start: 0,
end: 1,
outerAnimation: expandAnimation,
innerAnimation: expandContentsAnimation,
});
// Collapse animation.
this._append({
i,
step,
start: 1,
end: 0,
outerAnimation: collapseAnimation,
innerAnimation: collapseContentsAnimation,
});
}
ease.textContent = `
@keyframes expandAnimation {
${expandAnimation.join("")}
}
@keyframes expandContentsAnimation {
${expandContentsAnimation.join("")}
}
@keyframes collapseAnimation {
${collapseAnimation.join("")}
}
@keyframes collapseContentsAnimation {
${collapseContentsAnimation.join("")}
}`;
document.head.appendChild(ease);
return ease;
}
_append({ i, step, start, end, outerAnimation, innerAnimation } = opts) {
let scale = start + (end - start) * step;
let invScale = scale === 0 ? 0 : 1 / scale;
if (start === 0) {
if (i === 11) {
scale = 0.373;
invScale = 2.680965147453083;
}
if (i === 23) {
scale = 0.648;
invScale = 1.5432098765432098;
}
if (i === 28) {
scale = 0.7312;
invScale = 1.3676148796498906;
}
if (i === 41) {
scale = 0.879;
invScale =1.1376564277588168;
}
if (i === 43) {
scale = 0.894;
invScale = 1.1185682326621924;
}
if (i === 44) {
scale = 0.9;
invScale = 1.1111111111111112;
}
if (i === 55) {
scale = 0.959;
invScale = 1.0427528675703859;
}
if (i === 56) {
scale = 0.96;
invScale = 1.0416666666666667;
}
if (i === 62) {
scale = 0.97914;
invScale = 1.0213044099924424;
}
if (i === 64) {
scale = 0.983;
invScale = 1.017293997965412;
}
if (i === 67) {
scale = 0.988;
invScale = 1.0121457489878543;
}
if (i === 69) {
scale = 0.9907648;
invScale = 1.0093212839212697;
}
if (i === 72) {
scale = 0.99385;
invScale = 1.0061880565477688;
}
if (i === 74) {
scale = 0.99543;
invScale = 1.0045909807821745;
}
if (i === 85) {
scale = 0.99949;
invScale = 1.0005102602327187;
}
if (i === 89) {
scale = 0.9998536;
invScale = 1.0001464214360982;
}
if (i === 90) {
scale = 0.99995;
invScale = 1.000050002500125;
}
console.log(`${i}: scale: ${scale}, inverse: ${invScale}, scale * inverse = ${scale * invScale}`);
}
outerAnimation.push(`
${i}% {
transform: scaleY(${scale});
}`);
innerAnimation.push(`
${i}% {
transform: scaleY(${invScale});
}`);
}
_clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
_ease(v, pow = 4) {
v = this._clamp(v, 0, 1);
return 1 - Math.pow(1 - v, pow);
}
}
new Expando();
* {
box-sizing: border-box;
}
html,
body {
padding: 0;
font-family: Arial, Helvetica, sans-serif;
}
.expand-collapse-section,
.example-content-below-sections {
transition: transform .7s ease-out;
will-change: transform;
}
.section-header {
border: 1px solid #efefef;
border-radius: 4px;
padding: 0.5em;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-wrapper {
width: 36px;
height: 36px;
background: #f7f8f9;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.toggle {
background-image: url('data:image/svg+xml,<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23747878" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></svg>');
display: inline-block;
height: 24px;
width: 24px;
transition: transform 200ms;
}
.toggle.expanded {
transform: rotateZ(180deg);
}
.content {
transform: scaleY(0);
transform-origin: top left;
overflow: hidden;
will-change: transform;
margin-top: 0.5rem;
border: 1px solid black;
border-radius: 5px;
/* padding: 1em; */
display: none;
overflow: hidden;
perspective: 1px;
}
.content-inner {
transform-origin: top left;
overflow: hidden;
transform: scaleY(0);
will-change: transform;
}
/* need to be margin as with padding, transform makes it push to the top while animating collapse */
.content-inner-spacer {
margin: 1em;
}
.item--expanded {
animation-name: expandAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item__contents--expanded {
animation-name: expandContentsAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item--collapsed {
animation-name: collapseAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.item__contents--collapsed {
animation-name: collapseContentsAnimation;
animation-duration: .7s;
animation-timing-function: linear;
animation-fill-mode: forwards;
}
.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,minimum-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
<script src="script.js" defer></script>
</head>
<body>
<section class="expand-collapse-section first">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Neque,
reiciendis quasi laboriosam delectus accusantium aliquam! Alias
voluptatum, dolore debitis aliquam maxime doloribus eligendi tempora
amet laborum quis maiores reprehenderit deleniti impedit quo quas
eius, fugiat atque non accusamus eum esse? Explicabo quam ea
reprehenderit minus officiis vel et reiciendis ex omnis expedita, ab
libero veritatis! Suscipit, magni maxime deserunt eaque laborum libero
atque nesciunt labore consequatur provident. Aut dolor necessitatibus
sint, dicta facilis sed molestiae laudantium incidunt repellat
consequuntur, officiis maxime quam, dolorum possimus expedita minus.
Error quaerat, esse magni quibusdam quis corporis, et tenetur, ullam
ipsam a ratione fugit.
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<section class="expand-collapse-section">
<div class="section-header">
<div class="section-header-title">Expand / collapse</div>
<div class="toggle-wrapper">
<div class="toggle"></div>
</div>
</div>
<div class="content">
<div class="content-inner">
<div class="content-inner-spacer">
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
molestiae, quo ratione voluptas iure expedita dolor ad voluptate
maxime, aspernatur error sequi hic? Harum nobis provident recusandae
dolor, ea corporis minima animi deserunt, voluptate adipisci
cupiditate repudiandae inventore quasi commodi dolorum odio saepe
consequatur nulla. Fugit quo tenetur dolores veritatis!
</div>
</div>
</div>
</section>
<p class="example-content-below-sections">Bottom content</p>
</body>
</html>