<main>
  <button class="Button js-open-button">Open Drawer</button>
  
  <div class="Drawer js-drawer" hidden>    
    <button class="Button js-close-button">Close Drawer</button>
  </div>
</main>
.Drawer {
  background: #eee;
  box-shadow: -3px 0px 5px 0px rgba(0,0,0,0.6);  
  
  /* Give our drawer a base width */
  width: 100%;

  /* Position the drawer on the right edge of the screen */
  position: fixed;
  bottom: 0;
  right: 0;
  top: 0;

  /* Slide the drawer _past_ the right edge of the screen */
  transform: translateX(100%);

  /* Transition our horizontal slide */
  transition: transform ease-out 0.3s;
  
  /* Misc */
  
  text-align: center;
}

.Drawer.is-open {
  /* Slide the drawer back in when it's "open" */
  transform: translateX(0);
}

/**
 * The following styles are unrelated to our primary transition.
 */

html, body {
  height: 100%;
}

body {
  overflow-x: hidden;
}

main, .Drawer {
  align-items: center;
  display: flex;
  height:100%;
  justify-content: center;
}

.Button {
  background: #1990ea;
  border: none;
  border-radius: 10em;
  box-shadow: 0px 2px 5px 0px rgba(0,0,0,0.4);
  color: #fff;
  cursor: pointer;
  font-size: 2em;
  font-weight: bold;
  padding: 0.5em 1em;
  transition-property: background-color, box-shadow, transform;
  transition-timing-property: ease-out;
  transition-duration: 0.3s;
}

.Button:hover,
.Button:focus{
  background: #2395eb;
  box-shadow: 0px 4px 7px 0px rgba(0,0,0,0.4);
  transform: translateY(-2px);
}

.Button:active {
  background: #1c90e8;
  box-shadow: 0px 1px 2px 0px rgba(0,0,0,0.4);
  transform: translateY(0) scale(0.975);
}
const drawerTransitioner = transitionHiddenElement({
  element: document.querySelector('.js-drawer'),
  visibleClass: 'is-open'
});

document.querySelector('.js-open-button').addEventListener('click', () => {
  drawerTransitioner.show()
});

document.querySelector('.js-close-button').addEventListener('click', () => {
  drawerTransitioner.hide()
});

/** 
 * 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.