Design
Implementation

Drawer - Design

Drawers let you see a new set of actions or content without redirecting or routing.


 

Live Demo

 

Usage

Drawers are a slide out window that are opened with a disclosure button. They are used for a variety of things, but they generally contain some kind of actionable content, rather than just displaying information.

 

Anatomy

1. Drawer window
2. Content area
3. Close button
4. Drawer title
5. Heading area
 

Behavior

Drawers are hidden off any edge of the screen until its disclosure method is fired, and they have a max-width of 432px, with a min-width of 300px. On mobile they take up the whole screen.

Drawer - Implementation

Drawers let you see a new set of actions or content without redirecting or routing.


 

Implementation

<button class="btn" aria-label="Add to Cart Disclosure" type="button" aria-controls="drawer-0" aria-expanded="false" aria-haspopup="dialog">
  Select Options
</button>
<div class="tooltip" role="tooltip" hidden>Show Drawer</div>
<aside id="drawer-0" class="drawer drawer--position-right" role="dialog" aria-modal="true" aria-labelledby="drawer-0-title" tabindex="-1">
  <header class="drawer__header">
    <button class="btn drawer__close-btn" aria-label="Close Button" type="button">
      <svg focusable="false" role="presentation" class="icon" viewBox="0 0 24 24">
        <path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/>
      </svg>
      Back
    </button>
    <h2 id="drawer-0-title" class="drawer__title">Product Detail</h2>
  </header>
  <div class="drawer__content">
    Content goes here
    <div class="select">
      <label class="select__label select__label--floated" for="select-0">
        Size
      </label>
      <select class="select__native-control" id="select-0">
        <optgroup label="Select a size">
          <option>Small</option>
          <option>Medium</option>
          <option>Large</option>
        </optgroup>
      </select>
    </div>
    <button class="btn" type="button">
      Add to Cart
    </button>
  </div>
</aside>
<div class="drawer-scrim"></div>
<script>
    class Drawer {
      constructor(props) {
        this.lastFocusedElement = null;
        this.ref = props.ref;
        this.scrimRef = this.isElement(props.scrimRef) ? props.scrimRef : false;
        this.disclosure = this.isElement(props.disclosure) ? props.disclosure : false;
        this.closeBtn = this.isElement(props.closeBtn) ? props.closeBtn : false;
        this.isOpen = false;

        this.open = this.open.bind(this);
        this.close = this.close.bind(this);
        this.handleTransitionEnd = this.handleTransitionEnd.bind(this);
        this.handleKeydown = this.handleKeydown.bind(this);
        this.ref.addEventListener('transitionend', this.handleTransitionEnd);
        this.ref.addEventListener('keydown', this.handleKeydown, true);
        this.tabbableElements = this.getKeyboardFocusableElements(this.ref);
        const len = this.tabbableElements.length - 1;
        this.lastTabbableElement = this.tabbableElements[len];
        this.scrimRef && this.scrimRef.addEventListener('click', this.close);
        this.closeBtn && this.closeBtn.addEventListener('click', this.close);
      }

      getKeyboardFocusableElements(elem) {
        return Array.from(elem.querySelectorAll('a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])'))
          .filter(el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
      }

      isElement(o) {
        return (
          typeof HTMLElement === "object"
            ? o instanceof HTMLElement 
            : o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string"
        );
      }

      open() {
        this.isOpen = true;
        this.lastFocusedElement = document.activeElement;
        this.ref.classList.add('drawer--open');
        this.disclosure && this.disclosure.setAttribute('aria-expanded', true);
      }

      close() {
        this.isOpen = false;
        this.ref.classList.remove('drawer--open');
        this.disclosure && this.disclosure.setAttribute('aria-expanded', false);
      }

      handleKeydown(event) {
        if (event.key == 'Esc' || event.key == 'Escape') {
          this.close();
        }

        if (event.key == 'Tab') {
          if (!event.shiftKey && document.activeElement == this.lastTabbableElement) {
            event.preventDefault();
            this.tabbableElements[0].focus();
          }

          if (event.shiftKey && document.activeElement == this.tabbableElements[0]) {
            event.preventDefault();
            this.lastTabbableElement.focus();
          }
        }
      }

      handleTransitionEnd(event) {
        if (event.propertyName !== 'transform') return;
        if (this.isOpen) {
          this.tabbableElements[0].focus();
        } else {
          this.lastFocusedElement.focus();
        }
      }
    }

    const $drawerDisclosure = document.querySelector('.btn');
    const $tooltip = document.querySelector('.tooltip');

    const drawer = new Drawer({
      ref: document.querySelector('.drawer'),
      scrimRef: document.querySelector('.drawer-scrim'),
      disclosure: $drawerDisclosure,
      closeBtn: document.querySelector('.drawer__close-btn')
    });

    function handleClick() {
      drawer.open();
    }

    function handleMouseEnter() {
      $tooltip.hidden = false;
    }

    function handleMouseLeave() {
      $tooltip.hidden = true;
    }

    $drawerDisclosure && $drawerDisclosure.addEventListener('click', handleClick);
    $drawerDisclosure && $drawerDisclosure.addEventListener('mouseenter', handleMouseEnter);
    $drawerDisclosure && $drawerDisclosure.addEventListener('mouseleave', handleMouseLeave);
  </script>
.btn {
  -webkit-tap-highlight-color: transparent;
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1.875rem;
  text-decoration: none;
  text-transform: initial;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  min-width: 64px;
  height: 36px;
  padding: 0 16px;
  border: none;
  border-radius: 4px;
  outline: 0;
  background-color: transparent;
  overflow: hidden;
  -ms-touch-action: manipulation;
  touch-action: manipulation;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-appearance: none;
  appearance: none;
  gap: 4px;
  transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.btn:not(:disabled) {
  color: #212121;
  background-color: transparent;
}

.btn:hover {
  background-color: #f2f2f2;
  cursor: pointer;
}

.btn:after {
  content: "";
  box-shadow: inset 0 0 0 2px #000;
  width: 100%;
  height: 100%;
  z-index: 1;
  position: absolute;
  pointer-events: none;
  opacity: 0;
  border-radius: 4px;
  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.btn:focus:after {
  opacity: 1;
}

.icon {
  fill: #212121;
  width: 24px;
  height: 24px;
}

.drawer {
  position: fixed;
  display: flex;
  flex-flow: column wrap;
  left: 0;
  top: 0;
  height: 100vh;
  min-width: 300px;
  width:30%;
  background-color: #fff;
  border: 1px solid rgba(0, 0, 0, 0.12);
  transform: translate3d(-100%, 0, 0);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 9;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

.drawer--position-right {
  left: unset;
  right: 0;
  transform: translate3d(100%, 0, 0);
}

.drawer::after {
  content: "";
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  opacity: 0;
  transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 8px 10px -5px rgb(0 0 0 / 20%), 0 16px 24px 2px rgb(0 0 0 / 14%),
    0 6px 30px 5px rgb(0 0 0 / 12%);
}

.drawer--open {
  transform: translate3d(0, 0, 0);
}

.drawer--open::after {
  opacity: 1;
}

.drawer-scrim {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  opacity: 0;
  pointer-events: none;
  background-color: rgba(0, 0, 0, 0.32);
  transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 8;
}

.drawer--open + .drawer-scrim {
  opacity: 1;
  pointer-events: initial;
}

.drawer__header {
  width: 100%;
  height: 48px;
  display: grid;
  grid-template-columns: 72px 1fr;
  align-items: center;
  background-color: #f5f6f7;
  border-bottom: 1px solid #e5e5e5;
}

.drawer__title {
  font-weight: 500;
  font-size: 20px;
  margin: 0;
  line-height: 1;
  display: flex;
  justify-content: center;
}

.drawer__content {
  padding: 8px;
}

.drawer__close-btn {
  padding: 0;
  margin-left: 4px;
}

.select {
  position: relative;
  display: flex;
  flex-flow: column nowrap;
  margin: 0;
}

.select__label {
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
  font-size: 14px;
  line-height: 1;
  margin-bottom: 4px;
}

.select__label--floated {
  position: absolute;
  background-color: #fff;
  top: -6px;
  left: 8px;
  padding: 0 4px;
  font-size: 12px;
}

.select__native-control {
  -webkit-appearance: none;
  appearance: none;
  outline: 0;
  color: #212121;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: .04em;
  border: 1px solid #949499;
  border-radius: 4px;
  background-color: #fff;
  height: 36px;
  width: 100%;
  min-width: 88px;
  padding: 8px;
  transition: border 250ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: border, box-shadow;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23212121' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83z'%3E%3C/path%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right;
}

.select__native-control:hover {
  border-color: #000;
  box-shadow: inset 0 0 0 1px #000;
  cursor: pointer;
}

.select__native-control:focus {
  border-color: #3367d6 #285bc7 #2451b2;
  box-shadow: inset 0 0 0 1px #2451b2;
}

.tooltip {
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
  height: 24px;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #424548;
  color: #f8f9fa;
  font-size: 12px;
  letter-spacing: .7px;
  border-radius: 4px;
  width: max-content;
  position: relative;
  top: 4px;
}

[hidden] {
  display: none;
}

.page {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  font-weight: 400;
  line-height: 1.4em;
  font-size: 16px;
  direction: ltr;
  background-color: #fff;
  unicode-bidi: embed;
  text-rendering: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  font-synthesis: none;
  font-feature-settings: "liga", "kern";
}

.padding-8 {
  padding: 8px;
}