<button class="button modal-trigger" data-modal-trigger="myDialog" type="button" aria-expanded="false">
    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__container " tabindex="0">
        <div class="modal__content ">
            <div class="modal__top ">
                <h3 class="modal__heading heading heading--third-level" id="myTitle">
                    Save &quot;untitled&quot; document?
                </h3>

                <p class="modal__subheading ">
                    Some text
                </p>
            </div>

            <div class="modal__middle ">
                <div class="
                                modal__description
                                margin-bottom-xs
                            " id="myDesc">
                    You have made changes to &quot;untitled.txt&quot; that have not been saved. What do you want to do?
                </div>

                <button class="button " type="button" aria-label="button">
    I am a button
</button>

            </div>

            <div class="modal__bottom ">
                <div class="modal__bottom-actions ">
                    <div class="
                                    modal__bottom-action
                                    
                                ">
                        <button class="button button--secondary" type="button" aria-label="button">
    Cancel
</button>

                    </div>
                    <div class="
                                    modal__bottom-action
                                    
                                ">
                        <button class="button " type="button" aria-label="button">
    Save
</button>

                    </div>
                </div>
            </div>
        </div>

        <button class="button button--icon button--rotate-icon modal__close-button" type="button" aria-label="click to close the modal">
        <svg
    class="icon button__icon modal__close-button-icon"
    
        role="presentation"
        focusable="false"
>
        <title>Close</title>
    <use xlink:href="/images/icons-sprite.svg#close"></use>
</svg>

</button>

    </div>
</div>
<script type="text/javascript">
    new Modal(document.querySelector('[data-modal-trigger="myDialog"]'));
</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__container {{ modalContainer.class }}"
        {{{ modalContainer.attribtues }}}
        tabindex="0"
     >
        <div
            class="modal__content {{ modalContent.class }}"
            {{{ modalContent.attribtues }}}
        >
            {{#if modalTop }}
                <div class="modal__top {{ modalTopClass }}">
                    {{#if heading.text }}
                        <{{ heading.tag }}
                            class="modal__heading {{ heading.class }}"
                            {{{ heading.attributes }}}
                        >
                            {{ heading.text }}
                        </{{ heading.tag }}>
                    {{/if}}

                    {{#if subheading.text }}
                        <{{ subheading.tag }}
                            class="modal__subheading {{ subheading.class }}"
                            {{{ subheading.attributes }}}
                        >
                            {{ subheading.text }}
                        </{{ subheading.tag }}>
                    {{/if}}
                </div>
            {{/if}}

            {{#if modalMiddle }}
                <div class="modal__middle {{ modalMiddleClass }}">
                    {{#if modalDescription.text }}
                        <{{ modalDescription.tag }}
                            class="
                                modal__description
                                {{ modalDescription.class }}
                            "
                            {{{ modalDescription.attributes }}}
                        >
                            {{ modalDescription.text }}
                        </{{ modalDescription.tag }}>
                    {{/if}}

                    {{#if modalComponent.content }}
                        {{ render (component modalComponent.content) modalComponent.contentContext }}
                    {{/if}}
                </div>
            {{ else }}
                {{#if modalComponent.content }}
                    {{ render (component modalComponent.content) modalComponent.contentContext }}
                {{/if}}
            {{/if}}

            {{#if modalBottom }}
                <div class="modal__bottom {{ modalBottomClass }}">
                    <div class="modal__bottom-actions {{ modalBottomActionsClass }}">
                        {{#each modalBottomActions as |action| }}
                            <div
                                class="
                                    modal__bottom-action
                                    {{ action.class }}
                                "
                                {{ action.attributes }}
                            >
                                {{ render (component action.content) action.contentContext merge=true }}
                            </div>
                        {{/each}}
                    </div>
                </div>
            {{/if}}
        </div>

        {{#if buttonClose }}
            {{ render '@button--rotate-icon' buttonClose merge=true }}
        {{/if}}
    </div>
</div>
{{#if modal.id}}
    <script type="text/javascript">
        new Modal(document.querySelector('[data-modal-trigger="{{ modal.id }}"]'));
    </script>
{{/if}}
{
  "modalContainer": {
    "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\" aria-expanded=\"false\""
    }
  },
  "buttonClose": {
    "tag": "button",
    "text": "",
    "class": "button--rotate-icon modal__close-button",
    "icon": {
      "id": "close",
      "title": "Close",
      "class": "button__icon modal__close-button-icon"
    },
    "attributes": "type=\"button\" aria-label=\"click to close the modal\""
  },
  "modalTop": true,
  "heading": {
    "attributes": "id=\"myTitle\"",
    "tag": "h3",
    "class": "heading heading--third-level",
    "text": "Save \"untitled\" document?"
  },
  "subheading": {
    "tag": "p",
    "class": "",
    "text": "Some text"
  },
  "modalMiddle": true,
  "modalDescription": {
    "attributes": "id=\"myDesc\"",
    "class": "margin-bottom-xs",
    "tag": "div",
    "text": "You have made changes to \"untitled.txt\" that have not been saved. What do you want to do?"
  },
  "modalComponent": {
    "content": "button",
    "contentContext": ""
  },
  "modalBottom": true,
  "modalBottomActions": [
    {
      "content": "button--secondary",
      "contentContext": {
        "text": "Cancel"
      }
    },
    {
      "content": "button",
      "contentContext": {
        "text": "Save"
      }
    }
  ]
}
  • Content:
    // -----------------------------------------------------------
    // Please keep balance between Magento_UI and Alpaca z-indexes
    // -----------------------------------------------------------
    // COMPONENT - z-index OVERLAY / CONTENT (method)
    // -----------------------------------------------------------
    // Alpaca Modal - 99 / 99 (Styles)
    // Magento_UI Modal - 99 / 100 (Inlined via JS)
    // Magento_UI Confirmation Modal - 100 / 101 (Inlined via JS)
    // -----------------------------------------------------------
    $modal__z-index                              : $z-index-highest !default;
    $modal-ui-base__z-index                      : $z-index-highest + 1 !default;
    $modal-ui-confirm__z-index                   : $z-index-highest + 2 !default;
    // -----------------------------------------------------------
    
    $modal__padding                              : 0 !default;
    $modal__background-color                     : rgba(0, 0, 0, 0.7) !default;
    $modal__justify-content                      : center !default;
    $modal__align-items                          : center !default;
    
    //modal container
    $modal__container-width                      : calc(100% - (2 * #{$spacer--medium})) !default;
    $modal__container-max-width                  : 900px !default;
    $modal__container-padding                    : $spacer--semi-large $spacer--medium !default;
    $modal__container-padding\@medium            : $spacer--semi-large !default;
    $modal__container-background-color           : $white !default;
    $modal__container-border                     : none !default;
    $modal__container-box-shadow                 : $shadow !default;
    $modal__container-animation                  : animatetop 0.4s !default;
    
    //modal close
    $modal__close-button-top                     : $spacer--medium !default;
    $modal__close-button-right                   : $spacer--medium !default;
    
    //modal top
    $modal__top-padding                          : 0 0 $spacer--semi-medium 0 !default;
    $modal__top-margin                           : 0 !default;
    $modal__top-border                           : $border-base !default;
    $modal__top-border-width                     : 0 0 $border-width-base 0 !default;
    
    //modal middle
    $modal__middle-padding                       : $spacer--semi-large !default;
    $modal__middle-margin                        : 0 !default;
    $modal__middle-border                        : 0 !default;
    $modal__middle-border-width                  : 0 !default;
    
    //modal bottom
    $modal__bottom-padding                       : $spacer--semi-large 0 0 0 !default;
    $modal__bottom-margin                        : 0 !default;
    $modal__bottom-border                        : $border-base !default;
    $modal__bottom-border-width                  : $border-width-base 0 0 0 !default;
    
    //modal heading
    $modal__heading-font-size                    : $font-size-large !default;
    $modal__heading-font-weight                  : $font-weight-base !default;
    $modal__heading-margin                       : 0 !default;
    $modal__heading-height                       : 48px !default;
    $modal__heading-padding-right                : $spacer--extra-large !default;
    $modal__heading-padding-left                 : 0 !default;
    
    //modal subheading
    $modal__subheading-font-size                 : $font-size-base !default;
    $modal__subheading-font-weight               : $font-weight-base !default;
    $modal__subheading-margin                    : 0 !default;
    $modal__subheading-height                    : 24px !default;
    $modal__subheading-padding-right             : 0 !default;
    $modal__subheading-padding-left              : 0 !default;
    
    //modal bottom-actions
    $modal__bottom-actions-padding               : 0 !default;
    $modal__bottom-actions-margin                : 0 auto !default;
    $modal__bottom-actions-justify-content       : space-between !default;
    $modal__bottom-actions-max-width             : 320px !default;
    
    //modal bottom-action
    $modal__bottom-action-padding                : 0 !default;
    $modal__bottom-action-margin                 : 0 0 $spacer--medium 0 !default;
    $modal__bottom-action-width                  : calc(50% - #{$spacer}) !default;
    
    $modal__bottom-action-button-width           : 100% !default;
    
    //modal--secondary
    $modal__justify-content--secondary           : center !default;
    $modal__align-items--secondary               : flex-end !default;
    $modal__container-animation--secondary       : animateright 0.4s !default;
    $modal__container-height--secondary          : 100% !default;
    $modal__container-width--secondary           : 100% !default;
    $modal__container-max-width--secondary       : 640px !default;
    $modal__container-max-width--secondary\@large: 768px !default;
    $modal__content-height--secondary            : 100% !default;
    $modal__container-padding--secondary\@medium : $spacer--extra-large !default;
    $modal__container-padding--secondary\@large  : $spacer--extra-large 112px !default;
    
    //modal--tertiary
    $modal__justify-content--tertiary            : center !default;
    $modal__align-items--tertiary                : flex-start !default;
    $modal__container-animation--tertiary        : animateleft 0.4s !default;
    
    $modal__container-height--tertiary           : 100vh !default;
    $modal__container-width--tertiary            : 100% !default;
    $modal__container-max-width--tertiary        : 100% !default;
    $modal__container-box-shadow--tertiary       : none !default;
    
  • URL: /components/raw/modal/_modal-variables.scss
  • Filesystem Path: build/components/Organisms/modal/_modal-variables.scss
  • Size: 5.3 KB
  • Content:
    @import 'modal-variables';
    
    .modal {
        display: none;
        position: fixed;
        left: 0;
        top: 0;
        z-index: $modal__z-index;
        width: 100%;
        height: 100%;
        padding: $modal__padding;
        background-color: $modal__background-color;
    
        &--active {
            display: flex;
            flex-direction: column;
            justify-content: $modal__justify-content;
            align-items: $modal__align-items;
            &.side-menu__modal {
                height: 93.5vh;
                top: 6.5vh;
            }
        }
    
        &--secondary {
            justify-content: $modal__justify-content--secondary;
            align-items: $modal__align-items--secondary;
    
            .modal__container {
                height: $modal__container-height--secondary;
                width: $modal__container-width--secondary;
                max-width: $modal__container-max-width--secondary;
                animation: $modal__container-animation--secondary;
    
                @include mq($screen-m) {
                    padding: $modal__container-padding--secondary\@medium;
                }
    
                @include mq($screen-l) {
                    max-width: $modal__container-max-width--secondary\@large;
                    padding: $modal__container-padding--secondary\@large;
                }
            }
    
            .modal__content {
                height: $modal__content-height--secondary;
            }
        }
    
        &--tertiary {
            justify-content: $modal__justify-content--tertiary;
            align-items: $modal__align-items--tertiary;
    
            .modal__container {
                height: $modal__container-height--tertiary;
                width: $modal__container-width--tertiary;
                max-width: $modal__container-max-width--tertiary;
                animation: $modal__container-animation--tertiary;
                box-shadow: $modal__container-box-shadow--tertiary;
            }
        }
    
        &__container {
            position: relative;
            display: block;
            width: $modal__container-width;
            max-width: $modal__container-max-width;
            max-height: 100%;
            padding: $modal__container-padding;
            border: $modal__container-border;
            box-shadow: $modal__container-box-shadow;
            background-color: $modal__container-background-color;
            animation: $modal__container-animation;
    
            @include mq($screen-m) {
                padding: $modal__container-padding\@medium;
            }
        }
    
        &__content {
            display: flex;
            flex-direction: column;
            max-height: 100%;
    
            &--block {
                display: block;
            }
        }
    
        &__top {
            padding: $modal__top-padding;
            margin: $modal__top-margin;
            border: $modal__top-border;
            border-width: $modal__top-border-width;
        }
    
        &__middle {
            padding: $modal__middle-padding;
            margin: $modal__middle-margin;
            border: $modal__middle-border;
            border-width: $modal__middle-border-width;
            overflow-y: auto;
        }
    
        &__bottom {
            padding: $modal__bottom-padding;
            margin: $modal__bottom-margin;
            border: $modal__bottom-border;
            border-width: $modal__bottom-border-width;
        }
    
        &__close-button {
            position: absolute;
            top: $modal__close-button-top;
            right: $modal__close-button-right;
        }
    
        &__heading {
            font-size: $modal__heading-font-size;
            font-weight: $modal__heading-font-weight;
            margin: $modal__heading-margin;
    
            @include font-padding(
                $modal__heading-font-size,
                $modal__heading-height,
                $modal__heading-padding-right,
                $modal__heading-padding-left
            );
        }
    
        &__subheading {
            font-size: $modal__subheading-font-size;
            font-weight: $modal__subheading-font-weight;
            margin: $modal__subheading-margin;
    
            @include font-padding(
                $modal__subheading-font-size,
                $modal__subheading-height,
                $modal__subheading-padding-right,
                $modal__subheading-padding-left
            );
        }
    
        &__bottom-actions {
            display: flex;
            flex-wrap: wrap;
            justify-content: $modal__bottom-actions-justify-content;
            padding: $modal__bottom-actions-padding;
            margin: $modal__bottom-actions-margin;
            max-width: $modal__bottom-actions-max-width;
        }
    
        &__bottom-action {
            padding: $modal__bottom-action-padding;
            margin: $modal__bottom-action-margin;
            flex-basis: $modal__bottom-action-width;
    
            &:last-child {
                margin-bottom: 0;
            }
    
            .button {
                width: $modal__bottom-action-button-width;
            }
        }
    }
    
    // Magento_UI
    .modal-custom,
    .modal-popup {
        z-index: $modal-ui-base__z-index;
    }
    
    .modal-popup {
        &.confirm {
            z-index: $modal-ui-confirm__z-index;
        }
    }
    
  • URL: /components/raw/modal/_modal.scss
  • Filesystem Path: build/components/Organisms/modal/_modal.scss
  • Size: 4.8 KB
  • Content:
    'use strict';
    
    class Modal { // eslint-disable-line
      constructor(modalTrigger) {
        this.setListeners(modalTrigger);
      }
    
      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.trigger.setAttribute('aria-expanded', true);
        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.trigger.setAttribute('aria-expanded', false);
        modal.el.classList.remove(modal.activeClass);
        modal.focused.focus();
      }
      setListeners(modalTrigger) {
        const modal = {};
        modal.trigger = modalTrigger,
        modal.el = document.querySelector(`.modal[data-modal=${modal.trigger.dataset.modalTrigger}]`),
        modal.content     = modal.el.querySelector('.modal__content'),
        modal.closeButton = modal.el.querySelector('.modal__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     = '';
        modal.trigger.addEventListener('click',
          () => this.openModal(modal)
        );
        // clicking on button (x) closes the modal
        if (modal.closeButton) {
          modal.closeButton.addEventListener('click',
            () => this.closeModal(modal)
          );
        }
        // clicking anywhere outside of the modal closes 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)
          }
        });
        // escape key closes the modal
        window.addEventListener('keydown', (e) => {
          if (e.which === 27
            && modal.el.classList.contains(modal.activeClass)
          ) {
            this.closeModal(modal)
          }
        });
      }
    }
    
  • URL: /components/raw/modal/modal.js
  • Filesystem Path: build/components/Organisms/modal/modal.js
  • Size: 2.8 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.