<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
  };
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.