Modal

<button class="button modal-trigger" data-modal-trigger="myDialog" type="button">
        Modal trigger button
    </button>

    <div role="dialog" aria-hidden="true" id="myDialog" data-modal="myDialog" class="modal " aria-labelledby="myTitle" aria-describedby="myDesc">
        <div role="document" class="modal__content " tabindex="0">
            <h3 class="heading heading--third-level" id="myTitle"">
               Save &quot;untitled&quot; document?
            </h3>
            <div class=" modal__description "
                                 ">
                You have made changes to &quot;untitled.txt&quot; that have not been saved. What do you want to do?
        </div>
        <button class="button button--icon modal__js-close-button modal__close-button" type="button" aria-label="close modal button, click to close the modal">

            <svg class="icon button__icon modal__close-button-icon" role="img">
                <title>Close</title>
                <use xlink:href="/images/icons-sprite.svg#close"></use>
            </svg>

        </button>

    </div>
    </div>
    <script src="/components/raw/modal/modal.js"></script>
{{#if trigger }}
    {{ render '@modal-trigger' modalTrigger }}
{{/if}}
<div role="dialog"
     aria-hidden="true"
     id="{{ modal.id }}"
     data-modal="{{ modal.id }}"
     class="modal {{ modal.class }}"
     {{{ modal.attributes }}}
 >
    <div role="document"
         class="modal__content {{ modalContent.class }}"
         {{{ modalContent.attribtues }}}
         tabindex="0"
     >
        {{#if header.text }}
            <{{header.tag}} class="{{header.class}}" {{{ header.attributes }}}">
               {{header.text}}
            </{{header.tag}}>
        {{/if}}
        {{#if description.text }}
            <{{description.tag}} class="modal__description {{description.class}}"
                                 {{{ attributes }}}"
            >
               {{description.text}}
            </{{description.tag}}>
        {{/if}}
        {{#if main.content }}
            {{ render (component main.content) main.contentContext }}
        {{/if}}
        {{#if buttonClose }}
            {{ render '@button--icon' buttonClose merge=true}}
        {{/if}}
    </div>
</div>
{{#if script }}
    <script src="{{static 'modal.js' }}"></script>
{{/if}}
{
  "modalContent": {
    "class": ""
  },
  "modal": {
    "class": "",
    "id": "myDialog",
    "attributes": "aria-labelledby=\"myTitle\" aria-describedby=\"myDesc\""
  },
  "trigger": true,
  "modalTrigger": {
    "buttonModalTrigger": {
      "tag": "button",
      "class": "modal-trigger",
      "text": "Modal trigger button",
      "attributes": "data-modal-trigger=\"myDialog\" type=\"button\""
    }
  },
  "header": {
    "attributes": "id=\"myTitle\"",
    "tag": "h3",
    "class": "heading heading--third-level",
    "text": "Save \"untitled\" document?"
  },
  "description": {
    "attributes": "id=\"myDesc\"",
    "class": "",
    "tag": "div",
    "text": "You have made changes to \"untitled.txt\" that have not been saved. What do you want to do?"
  },
  "main": {
    "content": "",
    "contentContext": ""
  },
  "buttonClose": {
    "tag": "button",
    "text": "",
    "class": "button--icon modal__js-close-button modal__close-button",
    "icon": {
      "id": "close",
      "title": "Close",
      "class": "button__icon modal__close-button-icon"
    },
    "attributes": "type=\"button\" aria-label=\"close modal button, click to close the modal\""
  },
  "script": true
}
  • Content:
    $modal__padding                      : 0 !default;
    $modal__background-color             : rgba(0, 0, 0, 0.7) !default;
    
    $modal__description-margin           : 0 0 $spacer 0 !default;
    
    $modal__content-width                : calc(100% - (2 * #{$spacer--medium})) !default;
    $modal__content-width\@extra-large   : 80% !default;
    $modal__content-max-width            : 900px !default;
    $modal__content-background-color     : $white !default;
    $modal__content-border               : none !default;
    $modal__content-box-shadow           : 0 4px 6px 0 rgba(0, 0, 0, 0.3) !default;
    
    $modal__close-button-top             : $spacer !default;
    $modal__close-button-right           : $spacer !default;
    $modal__close-button-border          : none !default;
    $modal__close-button-background-color: $gray-darker !default;
    $modal__close-button-icon-color      : $white !default;
    
    .modal {
        position: fixed;
        left: 0;
        top: 0;
        z-index: 99;
        display: none;
        width: 100%;
        height: 100%;
        padding: $modal__padding;
        background-color: $modal__background-color;
        &--active {
            display: flex;
            justify-content: center;
            flex-direction: column;
            align-items: center;
        }
    
        &__description {
            margin: $modal__description-margin;
        }
    
        &__content {
            position: relative;
            display: block;
            width: $modal__content-width;
            max-width: $modal__content-max-width;
            padding: $spacer;
            border: $modal__content-border;
            box-shadow: $modal__content-box-shadow ;
            background-color: $modal__content-background-color;
            overflow-y: auto;
            animation-name: animatetop;
            animation-duration: 0.4s;
            @include mq($screen-xl) {
                width: $modal__content-width\@extra-large;
            }
        }
    
        &__close-button {
            border: $modal__close-button-border;
            border-radius: 0;
            background-color: $modal__close-button-background-color;
            &-icon {
                fill: $modal__close-button-icon-color;
                width: 20px;
                height: 20px;
            }
        }
    
        @keyframes animatetop {
            from {
                top: -300px;
                opacity: 0;
            }
            to {
                top: 0;
                opacity: 1;
            }
        }
    }
    
    body .modal-popup .modal-inner-wrap {
        margin: 1rem auto;
        height: 97vh;
        border-radius: 10px;
    }
    
    .modal-popup .modal-header {
        padding-top: 2.1rem;
        padding-bottom: 1.2rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    
    .modal-popup .modal-footer {
        margin-top: auto;
        display: flex;
        justify-content: flex-end;
        padding-top: 0;
        padding-bottom: 2rem;
    }
    
  • URL: /components/raw/modal/_modal.scss
  • Filesystem Path: build/components/03-modules/modal/_modal.scss
  • Size: 2.7 KB
  • Content:
    class Modal {
      constructor() {
        this.triggers = document.querySelectorAll('.modal-trigger');
        this.init();
      }
    
      trap(e, modal) {
        if (e.which == 27) {
          this.closeModal(modal);
        }
        if (e.which == 9) {
          let currentFocus = document.activeElement,
              totalOfFocusable = modal.focusableChildren.length,
              focusedIndex = modal.focusableChildren.indexOf(currentFocus);
          if (e.shiftKey) {
            if (focusedIndex === 0) {
              e.preventDefault();
              modal.focusableChildren[totalOfFocusable - 1].focus();
            }
          }
          else {
            if (focusedIndex == totalOfFocusable - 1) {
              e.preventDefault();
              modal.focusableChildren[0].focus();
            }
          }
        }
      }
    
      openModal(modal) {
        modal.focused = document.activeElement;
        modal.el.setAttribute('aria-hidden', false);
        modal.el.classList.add(modal.activeClass);
        modal.focusableChildren = Array.from(modal.el.querySelectorAll(modal.focusable));
        modal.focusableChildren[0].focus();
        modal.el.addEventListener('keydown', (e) => {
          this.trap(e, modal);
        });
      }
    
      closeModal(modal) {
        modal.el.setAttribute('aria-hidden', true);
        modal.el.classList.remove(modal.activeClass);
        modal.focused.focus();
      }
    
      setListeners() {
        this.triggers.forEach(trigger => {
          const modal = {};
          modal.triggerId   = trigger.dataset.modalTrigger,
          modal.el          = document.querySelector(`.modal[data-modal=${modal.triggerId}]`),
          modal.content     = modal.el.querySelector('.modal__content'),
          modal.closeButton = [...modal.el.querySelectorAll('.modal__js-close-button')],
          modal.activeClass = 'modal--active',
          modal.focusable   = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), object, embed, *[tabindex], *[contenteditable]',
          modal.focused     = '';
    
          /// When the user clicks on trigger, open the modal
          trigger.addEventListener('click',
            () => this.openModal(modal)
          );
    
          // When the user clicks on button (x), close the modal
          if (modal.closeButton.length > 0) {
            modal.closeButton.forEach(closeButton => {
              closeButton.addEventListener('click',
                () => this.closeModal(modal)
              );
            });
          }
    
          // When the user clicks anywhere outside of the modal, close the modal
          window.addEventListener('click', (e) => {
            if (e.target === modal.el
              && modal.el.classList.contains(modal.activeClass)
              && !modal.content.contains(e.target)
            ) {
              this.closeModal(modal)
            }
          });
    
          // When the user push escape, close the modal
          window.addEventListener('keydown', (e) => {
            if (e.which === 27
              && modal.el.classList.contains(modal.activeClass)
            ) {
              this.closeModal(modal)
            }
          });
        })
      }
    
      init() {
        this.setListeners();
      }
    }
    
    new Modal();
    
  • URL: /components/raw/modal/modal.js
  • Filesystem Path: build/components/03-modules/modal/modal.js
  • Size: 3 KB

Modal component

Accessibility features for modal component

During implementation, please add to div[role="dialog"] two aria attributes which help screen readers to tell what is modal about:

  • aria-labelledby - as a value pass the id of title element, the text if this element will be written
  • aria-describedby - as a value pass the id of description element, the text if this element will be written

Code example:

<div role="dialog"
     aria-hidden="true"
     id="myDialog"
     data-modal="myDialog"
     class="modal"
     tabindex="-1"
     aria-labelledby="myTitle"
     aria-describedby="myDesc"
 >
    <div role="document" class="modal__content" tabindex="0">
         <div id="myTitle">Save "untitled" document?</div>
         <div id="myDesc">You have made changes to "untitled.txt" that have not been saved. What do you want to do?</div>
         <button id="saveMe" type="button">Save changes</button>
         <button id="discardMe" type="button">Discard changes</button>
         <button id="neverMind" type="button">Cancel</button>
    </div>
</div>

If you don’t use neither title or description element, use at least aria-label attribute for it.