<main class="main-content">
<h1 class="title-content">Expandable Animation Keyframe</h1>
<article class="middle-content">
<div class="section-item-wrapper">
<div class="section is--collapsed">
<div class="section-item">
<button id="section-title" aria-expanded="false" aria-controls="section-content" class="section-item-title">
Rutger Hauer
<i class="section-item-title-icon fas fa-chevron-down"></i>
</button>
<div id="section-content" role="region" tabindex="-1" aria-labelledby="section-title" class="section-item-content" aria-hidden="true">
<q>"I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain."</q>
</div>
</div>
</div>
</div>
</article>
<article class="bottom-content">
<a class="social-icon" title="Go to Twitter" target="_blank" href="https://twitter.com/Bernardoagc1990">
<i class="fab fa-twitter"></i>
</a>
<a class="social-icon" title="Go to Linkedin" target="_blank" href="https://www.linkedin.com/in/bernardo-cardoso-0a740b95/">
<i class="fab fa-linkedin-in"></i>
</a>
<a class="social-icon" title="Go to Medium" target="_blank" href="https://medium.com/@bernardocardoso">
<i class="fab fa-medium-m"></i>
</a>
</article>
</main>
/* Design Systems styles variables */
:root {
/* Colors */
--color-primary: #1068eb;
--color-background: #f4f4f4;
--color-neutral: #6e6c6c;
/* Spacing */
--space-xs: 4px;
--space-s: 8px;
--space-base: 16px;
/* Font */
--font-family: "Nunito Sans", Arial, Helvetica, sans-serif;
--font-size-base: 16px;
--font-size-m: 24px;
--heading-1: 36px;
/* Styles */
--border-radius-soft: 4px;
--shadow-positive: 3px 3px 6px #bebebe, -3px -3px 6px #fff;
--shadow-negative: inset 3px 3px 5px #cbcbcb, inset -3px -3px 5px #fff;
}
/* Resets */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Html structure and grid*/
html, body {
background-color: var(--color-background);
font-family: var(--font-family);
font-size: var(--font-size-base);
height: 100%;
width: 100%;
}
.main-content {
display: grid;
grid-template-rows: 200px 1fr 1fr;
height: 100%;
padding-top: var(--space-base);
width: 100%;
}
.main-content .middle-content,
.main-content .title-content,
.main-content .bottom-content {
align-self: center;
justify-self: center;
}
.title-content {
font-size: var(--heading-1);
text-align: center;
}
.middle-content {
min-height: 200px;
width: 300px;
}
.bottom-content {
}
.bottom-content .social-icon {
align-items: center;
box-shadow: var(--shadow-positive);
color: var(--color-neutral);
display: inline-flex;
height: 52px;
justify-content: center;
margin-right: var(--space-base);
padding: var(--space-base);
text-decoration: none;
width: 52px;
}
.bottom-content .social-icon:hover {
box-shadow: var(--shadow-negative);
color: var(--color-primary);
}
/* Section Pattern Styles*/
.section-item-wrapper {
min-height: var(--title-height);
position: relative;
}
.section {
animation-duration: 600ms;
animation-timing-function: step-end;
background: linear-gradient(145deg, #fff, #dcdcdc);
box-shadow: var(--shadow-positive);
contain: content;
left: 0;
position: absolute;
top: 0;
transform-origin: top left;
will-change: transform;
}
.section.is--expanded {
background: var(--color-background);
box-shadow: var(--shadow-negative);
position: static;
}
.section-item {
animation-duration: 600ms;
animation-timing-function: step-end;
contain: content;
transform-origin: top left;
will-change: transform;
}
.section-item-title {
align-items: center;
background-color: transparent;
border: none;
color: var(--color-neutral);
cursor: pointer;
display: inline-flex;
justify-content: space-between;
font-family: inherit;
font-size: var(--font-size-m);
padding: var(--space-base);
width: 100%;
}
.section-item-title-icon {
color: var(--color-primary);
font-size: var(--font-size-base);
transition: transform 200ms cubic-bezier(0.4, 0, 1, 1);
transform-origin: center;
}
.is--expanded .section-item-title-icon {
transform: rotate(180deg);
}
.section-item-content {
color: var(--color-neutral);
opacity: 1;
padding: var(--space-base);
transition: opacity 500ms ease;
}
.is--collapsed .section-item-content {
opacity: 0;
visibility: hidden;
}
/* Animations */
.section.is--expanded {
animation-name: var(--sectionExpandAnimation);
}
.is--expanded .section-item {
animation-name: var(--sectionExpandContentsAnimation);
}
.section.is--collapsed {
animation-name: var(--sectionCollapseAnimation);
}
.is--collapsed .section-item {
animation-name: var(--sectionCollapseContentsAnimation);
}
// Constructor based on Google's ChromeLabs https://github.com/GoogleChromeLabs/ui-element-samples
class section {
constructor (elem, index) {
this._section = elem;
this._sectionWrapper = this._section.parentNode;
this._sectionItem = this._section.querySelector('.section-item');
this._sectionItemTitle = this._section.querySelector('.section-item-title');
this._sectionContent = this._section.querySelector('.section-item-content');
this._expanded = true;
this._animate = false;
this._nFrames = 60;
this._collapsed;
this._index = index;
this.expand = this.expand.bind(this);
this.collapse = this.collapse.bind(this);
this.toggle = this.toggle.bind(this);
this._calculateScales();
this._createEaseAnimations(index);
this._addEventListeners();
this.collapse();
this._animate = true;
this._sectionExpandAnimationName = '';
this._sectionExpandContentsAnimationName = '';
this._sectionCollapseAnimationName = '';
this._sectionCollapseContentsAnimationName = '';
}
collapse () {
if (!this._expanded) {
return;
}
this._expanded = false;
var y = this._collapsed.y;
var invY = 1 / y;
this._section.style.transform = `scaleY(${y})`;
this._sectionItem.style.transform = `scaleY(${invY})`;
this._handleAccessbility(false);
if (!this._animate) {
return;
}
this._applyAnimation({expand: false});
}
expand () {
if (this._expanded) {
return;
}
this._expanded = true;
this._section.style.transform = `scaleY(1)`;
this._sectionItem.style.transform = `scaleY(1)`;
this._handleAccessbility(true);
if (!this._animate) {
return;
}
this._applyAnimation({expand: true});
}
toggle () {
if (this._expanded) {
requestAnimationFrame(this.collapse);
return;
}
requestAnimationFrame(this.expand);
}
_handleAccessbility (isExpand) {
var tabindexValue = isExpand ? 0 : -1;
this._sectionItemTitle.setAttribute('aria-expanded', isExpand);
this._sectionContent.setAttribute('aria-hidden', !isExpand);
this._sectionContent.setAttribute('tabindex', tabindexValue);
}
_addEventListeners () {
this._sectionItemTitle.addEventListener('click', this.toggle);
}
_applyAnimation ({expand}=opts) {
this._section.classList.remove('is--expanded');
this._section.classList.remove('is--collapsed');
if (expand) {
this._section.classList.add('is--expanded');
return;
}
this._section.classList.add('is--collapsed');
}
_calculateScales () {
var collapsed = this._sectionItemTitle.getBoundingClientRect();
var expanded = this._section.getBoundingClientRect();
// create css variable with collapsed height, to apply on the wrapper
this._sectionWrapper.style.setProperty('--title-height', parseInt(collapsed.height) + 'px');
this._collapsed = {
y: collapsed.height / expanded.height
}
}
_createEaseAnimations (index) {
var sectionEase = document.querySelector('.section-animations');
if (!sectionEase) {
sectionEase = document.createElement('style');
sectionEase.classList.add('section-animations');
}
var sectionExpandAnimation = [];
var sectionExpandContentsAnimation = [];
var sectionCollapseAnimation = [];
var sectionCollapseContentsAnimation = [];
var percentIncrement = 100 / this._nFrames;
for (var i = 0; i <= this._nFrames; i++) {
var step = this._ease(i / this._nFrames).toFixed(5);
var percentage = (i * percentIncrement).toFixed(5);
var startY = this._collapsed.y;
var endY = 1;
// Expand animation.
this._append({
percentage,
step,
startY,
endY,
outerAnimation: sectionExpandAnimation,
innerAnimation: sectionExpandContentsAnimation
});
// Collapse animation.
this._append({
percentage,
step,
startY: 1,
endY: this._collapsed.y,
outerAnimation: sectionCollapseAnimation,
innerAnimation: sectionCollapseContentsAnimation
});
}
// Create unique Animation names, useful for multiple section patterns
this._createAnimationsNames();
sectionEase.textContent += `
@keyframes ${this._sectionExpandAnimationName} {
${sectionExpandAnimation.join('')}
}
@keyframes ${this._sectionExpandContentsAnimationName} {
${sectionExpandContentsAnimation.join('')}
}
@keyframes ${this._sectionCollapseAnimationName} {
${sectionCollapseAnimation.join('')}
}
@keyframes ${this._sectionCollapseContentsAnimationName} {
${sectionCollapseContentsAnimation.join('')}
}`;
document.head.appendChild(sectionEase);
return sectionEase;
}
_append ({
percentage,
step,
startY,
endY,
outerAnimation,
innerAnimation}=opts) {
var yScale = (startY + (endY - startY) * step).toFixed(5);
var invScaleY = (1 / yScale).toFixed(5);
outerAnimation.push(`
${percentage}% {
transform: scaleY(${yScale});
}`);
innerAnimation.push(`
${percentage}% {
transform: scaleY(${invScaleY});
}`);
}
_createAnimationsNames () {
this._sectionExpandAnimationName = "sectionExpandAnimation" + this._index;
this._sectionExpandContentsAnimationName = "sectionExpandContentsAnimation" + this._index;
this._sectionCollapseAnimationName = "sectionCollapseAnimation" + this._index;
this._sectionCollapseContentsAnimationName = "sectionCollapseContentsAnimation" + this._index;
// Create CSS Var of each animation
this._section.style.setProperty('--sectionExpandAnimation', this._sectionExpandAnimationName);
this._section.style.setProperty('--sectionExpandContentsAnimation', this._sectionExpandContentsAnimationName);
this._section.style.setProperty('--sectionCollapseAnimation', this._sectionCollapseAnimationName);
this._section.style.setProperty('--sectionCollapseContentsAnimation', this._sectionCollapseContentsAnimationName);
}
_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);
}
}
var sections = document.querySelectorAll('.section');
var currentsection;
for(var i = 0; i < sections.length; i++){
currentsection = sections[i];
new section(currentsection, i);
}
This Pen doesn't use any external JavaScript resources.