<main style="--length: 6;">
<header>
<h1>Famous Elephants</h1>
<button class="Button js-toggle-button">
<span class="Button__show-text js-show-text is-shown">Show</span>
<span class="Button__hide-text js-hide-text" hidden>Hide</span>
</button>
</header>
<nav>
<ul class="Menu js-menu">
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 0;">Babar</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 1;">Dumbo</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 2;">Echo</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 3;">Jumbo</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 4;">Packy</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link js-link" style="--index: 5;">Ruby</a>
</li>
</ul>
</nav>
<p>
Click the button above to see a list of famous elephants.
</p>
</main>
.Menu {
list-style: none;
margin: 0;
padding: 0;
position: absolute;
width: 100%;
z-index: 1;
}
.Menu__link {
--index: 0;
align-items: center;
background: #dde;
border-top: 1px solid #aac;
color: #002;
display: flex;
font-size: 1.25em;
justify-content: space-between;
line-height: 1;
opacity: 0;
padding: 1em;
text-decoration: none;
transform: translateX(100%);
transition-delay: calc(0.025s * (var(--length) - (var(--index) + 1)));
transition-duration: 0.2s;
transition-timing-function: ease-out;
transition-property: opacity, transform;
}
.Menu__link::after {
align-items: center;
border-right: 1px solid #002;
border-top: 1px solid #002;
content: "";
display: flex;
height: 0.25em;
font-size: 2em;
justify-content: center;
margin-right: 0.25em;
transform: rotate(45deg);
width: 0.25em;
}
.Menu.is-open .Menu__link {
opacity: 1;
transform: translateX(0);
transition-delay: calc(0.025s * var(--index));
}
/**
* Unrelated CSS
*
* The following CSS is used to lay out aspects of the demo
* unrelated to our staggered animations.
*/
body {
background: #334;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";}
main {
background: #eef;
color: #002;
height: 27em;
margin: 0 auto;
max-width: 20em;
overflow: hidden;
position: relative;
}
header {
align-items: center;
background: #dde;
display: flex;
justify-content: space-between;
padding: 1em;
position: relative;
z-index: 2;
}
h1 {
font-size: 1.25em;
margin: 0;
}
.Button {
background: #33c;
border: none;
border-radius: 10em;
color: #fff;
cursor: pointer;
font-size: 1em;
height: 2em;
position: relative;
transition: background ease-out 0.3s;
text-align: center;
width: 5em;
}
.Button__hide-text,
.Button__show-text {
left: 0;
opacity: 0;
position: absolute;
right: 0;
top: 0.35em;
text-align: center;
transition: opacity ease-out 0.3s;
}
.Button__hide-text.is-shown,
.Button__show-text.is-shown {
opacity: 1;
}
p {
line-height: 1.4;
padding: 0 1em;
}
const button = document.querySelector('.js-toggle-button');
const menu = document.querySelector('.js-menu');
const links = document.querySelectorAll('.js-link');
const showTextTransitioner = transitionHiddenElement({
element: document.querySelector('.js-show-text'),
visibleClass: 'is-shown',
});
const hideTextTransitioner = transitionHiddenElement({
element: document.querySelector('.js-hide-text'),
visibleClass: 'is-shown',
})
const transitionDelay = 0.025;
const transitionLength = 0.2;
const itemLength = 6;
const staggeredTransitionLength = (transitionDelay * (itemLength - 1)) + transitionLength;
const menuTransitioner = transitionHiddenElement({
element: document.querySelector('.js-menu'),
visibleClass: 'is-open',
waitMode: 'timeout',
timeoutDuration: staggeredTransitionLength * 1000
})
button.addEventListener('click', () => {
toggleMenu();
});
links.forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
toggleMenu();
});
});
const toggleMenu = () => {
showTextTransitioner.toggle();
hideTextTransitioner.toggle();
menuTransitioner.toggle();
}
/**
* Library code
* Using https://www.npmjs.com/package/@cloudfour/transition-hidden-element
*/
function transitionHiddenElement({
element,
visibleClass,
waitMode = 'transitionend',
timeoutDuration,
hideMode = 'hidden',
displayValue = 'block'
}) {
if (waitMode === 'timeout' && typeof timeoutDuration !== 'number') {
console.error(`
When calling transitionHiddenElement with waitMode set to timeout,
you must pass in a number for timeoutDuration.
`);
return;
}
// Don't wait for exit transitions if a user prefers reduced motion.
// Ideally transitions will be disabled in CSS, which means we should not wait
// before adding `hidden`.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
waitMode = 'immediate';
}
/**
* An event listener to add `hidden` after our animations complete.
* This listener will remove itself after completing.
*/
const listener = e => {
// Confirm `transitionend` was called on our `element` and didn't bubble
// up from a child element.
if (e.target === element) {
applyHiddenAttributes();
element.removeEventListener('transitionend', listener);
}
};
const applyHiddenAttributes = () => {
if(hideMode === 'display') {
element.style.display = 'none';
} else {
element.setAttribute('hidden', true);
}
}
const removeHiddenAttributes = () => {
if(hideMode === 'display') {
element.style.display = displayValue;
} else {
element.removeAttribute('hidden');
}
}
return {
/**
* Show the element
*/
show() {
/**
* This listener shouldn't be here but if someone spams the toggle
* over and over really fast it can incorrectly stick around.
* We remove it just to be safe.
*/
element.removeEventListener('transitionend', listener);
/**
* Similarly, we'll clear the timeout in case it's still hanging around.
*/
if (this.timeout) {
clearTimeout(this.timeout);
}
removeHiddenAttributes();
/**
* Force a browser re-paint so the browser will realize the
* element is no longer `hidden` and allow transitions.
*/
const reflow = element.offsetHeight;
element.classList.add(visibleClass);
},
/**
* Hide the element
*/
hide() {
if (waitMode === 'transitionend') {
element.addEventListener('transitionend', listener);
} else if (waitMode === 'timeout') {
this.timeout = setTimeout(() => {
applyHiddenAttributes();
}, timeoutDuration);
} else {
applyHiddenAttributes();
}
// Add this class to trigger our animation
element.classList.remove(visibleClass);
},
/**
* Toggle the element's visibility
*/
toggle() {
if (this.isHidden()) {
this.show();
} else {
this.hide();
}
},
/**
* Tell whether the element is hidden or not.
*/
isHidden() {
/**
* The hidden attribute does not require a value. Since an empty string is
* falsy, but shows the presence of an attribute we compare to `null`
*/
const hasHiddenAttribute = element.getAttribute('hidden') !== null;
const isDisplayNone = element.style.display === 'none';
const hasVisibleClass = [...element.classList].includes(visibleClass);
return hasHiddenAttribute || isDisplayNone || !hasVisibleClass;
},
// A placeholder for our `timeout`
timeout: null
};
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.