<div id="example-modal" class="cwf-modal" aria-hidden="true">
    <div class="cwf-modal__overlay" tabindex="-1">
        <button class="cwf-modal__close" aria-label="Close modal" tabindex="-1">
            <span>Close</span>
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" class="cwf-modal__times" role="presentation">
                <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
                <path fill="currentColor" d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z" />
            </svg> </button>
        <div class="cwf-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="cwf-modal__title" tabindex="-1">
            <span id="cwf-modal__title" class="cwf-modal__title">
                Modal example
            </span>
            <div class="cwf-modal__content">
                <p>
                    Any HTML can go in here. Lorem ipsum dolor sit amet, consectetur
                    <a href="https://www.vcu.edu/">Virginia Commonwealth University</a>
                    adipiscing elit. Nulla tristique quam in purus eleifend maximus. Nullam sed
                    magna eget leo consequat faucibus in vitae magna.
                </p>
            </div>
        </div>
    </div>
</div>
        
    
        <div id="{{ id ?? 'cwf-modal' }}" class="cwf-modal" aria-hidden="true">
    <div class="cwf-modal__overlay" tabindex="-1">
        <button class="cwf-modal__close" aria-label="Close modal" tabindex="-1">
            <span>Close</span>
            {% include '../../shared/icons/times-solid.svg' with {
                class: 'cwf-modal__times',
                role: 'presentation'
            } %}
        </button>
        <div class="cwf-modal__dialog"
            role="dialog"
            aria-modal="true"
            aria-labelledby="cwf-modal__title"
            tabindex="-1">
            <span id="cwf-modal__title" class="cwf-modal__title">
                {{ title }}
            </span>
            <div class="cwf-modal__content">
                {{ content }}
            </div>
        </div>
    </div>
</div>
    
        
            
            {
  "id": "example-modal",
  "title": "Modal example",
  "content": "<p>\n    Any HTML can go in here. Lorem ipsum dolor sit amet, consectetur\n    <a href=\"https://www.vcu.edu/\">Virginia Commonwealth University</a>\n    adipiscing elit. Nulla tristique quam in purus eleifend maximus. Nullam sed\n    magna eget leo consequat faucibus in vitae magna.\n</p>"
}
            
        
    
                                // Modal component styles
@use "../../shared/animation";
@use "../../shared/media";
@use "../../shared/style";
@use "../../shared/theme";
// Selector prefix
$prefix: "cwf" !default;
.#{$prefix}-modal {
    display: none;
}
.#{$prefix}-modal--open {
    display: block;
}
$overlay__background-color: style.opacity("black", 75%) !default;
$overlay__background-color--reduced-transparency: style.color("black") !default;
.#{$prefix}-modal__overlay {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: calc(1rem - 4px);
    background-color: var(--cwf-modal__overlay--background-color);
    will-change: transform;
    @include style.z-index("modal");
    --cwf-modal__overlay--background-color: #{$overlay__background-color};
    @include media.reduced(transparency) {
        --cwf-modal__overlay--background-color: #{$overlay__background-color--reduced-transparency};
    }
    @include media.reduced(transparency, no-preference) {
        --cwf-modal__overlay--background-color: #{$overlay__background-color};
    }
}
.#{$prefix}-modal[aria-hidden="false"] .#{$prefix}-modal__overlay {
    @include animation.animation--fadeIn;
}
.#{$prefix}-modal[aria-hidden="true"] .#{$prefix}-modal__overlay {
    @include animation.animation--fadeOut;
}
$dialog__background-color: style.color("white") !default;
$dialog__border-color: var(--cwf-modal__dialog--background-color) !default;
$dialog__outline--focus: none !default;
$dialog__border-color--focus: style.color("black") !default;
.#{$prefix}-modal__dialog {
    box-sizing: border-box;
    max-width: 900px;
    max-height: 100vh;
    padding: 2rem;
    border-radius: 0.5rem;
    background-color: var(--cwf-modal__dialog--background-color);
    border: 2px solid var(--cwf-modal__dialog--background-color);
    overflow-y: auto;
    will-change: transform;
    --cwf-modal__dialog--background-color: #{$dialog__background-color};
    &:focus {
        outline: $dialog__outline--focus;
        border-color: $dialog__border-color--focus;
    }
}
.#{$prefix}-modal[aria-hidden="false"] .#{$prefix}-modal__dialog {
    @include animation.animation--slideInUp;
}
.#{$prefix}-modal[aria-hidden="true"] .#{$prefix}-modal__dialog {
    @include animation.animation--slideOutDown;
}
.#{$prefix}-modal__title {
    display: block;
    font-size: 1.25rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
}
$close__background-color: style.color("black") !default;
$close__color: style.color("white") !default;
$close__background-color--active: style.color("black") !default;
$close__background-color--desktop: style.opacity("black", 50%) !default;
.#{$prefix}-modal__close {
    position: absolute;
    top: 0;
    right: 0;
    display: flex;
    align-items: center;
    justify-content: space-evenly;
    min-width: 128px;
    height: 64px;
    padding: 0;
    border: none;
    background-color: var(--cwf-modal__close--background-color);
    font-family: theme.font--sans-serif();
    font-size: 1rem;
    font-weight: 700;
    color: var(--cwf-modal__close--color);
    @include animation.transition(background-color);
    --cwf-modal__close--background-color: #{$close__background-color};
    --cwf-modal__close--color: #{$close__color};
    --cwf-modal__close--active--background-color: #{$close__background-color--active};
    &:hover,
    &:focus {
        background-color: var(--cwf-modal__close--active--background-color);
    }
    @include media.breakpoint {
        --cwf-modal__close--background-color: #{$close__background-color--desktop};
    }
}
.#{$prefix}-modal__close span,
.#{$prefix}-modal__times {
    pointer-events: none;
}
.#{$prefix}-modal__times {
    min-width: 1.125rem;
    width: 1.125rem;
    margin-left: 0.25rem;
}
.#{$prefix}-modal__content {
    @include style.children;
}
                            
                            
                        
                                // The default component class
import { Component } from '../../shared/component.js';
// Lock/unlock document scrolling
import { toggleScrollLock } from '../../shared/event.js';
// Traps the focus to a specifc set of elements
import { trap as trapFocus } from '../../shared/focus.js';
// Find focusable descendants and toggle an element's tab order
import {
    descendants as getFocusableDescendants,
    toggle as toggleTabOrder
} from '../../shared/focus.js';
// Provide event driven functionality to the modals
export class Modal extends Component {
    // When the class is instantiated
    constructor({
        prefix = 'cwf',
        modal = 'modal',
        open = 'modal--open',
        overlay = 'modal__overlay',
        close = 'modal__close',
        dialog = 'modal__dialog'
    } = {}) {
        super({
            prefix,
            classes: {
                modal,
                open,
                overlay,
                close,
                dialog
            }
        });
        // For each event listener, bind this to it
        this.onClick = this.onClick.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);
        this.onAnimationEnd = this.onAnimationEnd.bind(this);
        this.onHashChange = this.onHashChange.bind(this);
        this.setup = this.setup.bind(this);
        // Register the toggle scroll lock...
        this.toggleScrollLock = toggleScrollLock.bind(this);
        // ... and trap focus functions to this component
        this.trapFocus = trapFocus.bind(this);
    }
    // Clear the location hash
    clearHash() {
        // Remove the location hash...
        window.location.hash = '';
        // ... without jumping back to the top of the page...
        document.scrollingElement.scrollTop = this.scrollTop;
        document.scrollingElement.scrollLeft = this.scrollLeft;
        // ... and if possible, remove the hash altogether
        if ('replaceState' in history)
            return history.replaceState(null, null, ' ');
    }
    // Handle click events
    onClick(overlay, close, { target }) {
        // If the overlay or close button were clicked, clear the location hash
        if ([overlay, close].includes(target)) return this.clearHash();
    }
    // Handle key down events
    onKeyDown(event) {
        // Attempt to find a modal that's actively open...
        const reference = this.references.find(
            ({ state }) => state.target && state.open
        );
        // ... and if none exist, do nothing else
        if (!reference) return;
        // Grab the key pressed
        const { key } = event;
        // If the escape key was pressed, clear the location hash
        if (key === 'Escape') return this.clearHash();
        // Otherwise, grab the dialog, focusables, and close button from the modal...
        const { dialog, focusables, close } = reference;
        // ... and if the tab key was pressed, trap the focus to them
        if (key === 'Tab')
            return this.trapFocus([dialog, ...focusables, close], event);
    }
    // Handle animation end events
    onAnimationEnd({ currentTarget: modal }) {
        // Remove this event handler...
        modal.removeEventListener('animationend', this.onAnimationEnd);
        // ... and the modal's open class
        modal.classList.remove(this.classes.open);
    }
    // Close the modal if open
    closeModal({ modal, state, overlay, dialog, focusables, close }) {
        // Close the modal
        state.open = false;
        modal.setAttribute('aria-hidden', true);
        // Next, remove all focusable elements from the tab order
        toggleTabOrder(false, overlay, dialog, ...focusables, close);
        // Finally, bind to its animation end event...
        modal.addEventListener('animationend', this.onAnimationEnd);
        // ... and unlock document scrolling
        this.toggleScrollLock();
    }
    // Open the given modal
    openModal({ modal, state, overlay, dialog, focusables, close }) {
        // Open the modal
        modal.setAttribute('aria-hidden', false);
        modal.classList.add(this.classes.open);
        state.open = true;
        // Add all focusable elements to the tab order
        toggleTabOrder(true, overlay, dialog, ...focusables, close);
        // Finally, focus the dialog box...
        dialog.focus();
        // ... and lock document scrolling
        this.toggleScrollLock();
    }
    // Toggle the given modal open or close
    toggleModal(reference) {
        // Grab the modal's state
        const { state } = reference;
        const { target, open } = state;
        // If the modal is already in its correct state, do nothing else
        if ((target && open) || (!target && !open)) return;
        // Otherwise, open it if it's the target...
        if (target) return this.openModal(reference);
        // ... and close it if not
        return this.closeModal(reference);
    }
    // Handle hash change events
    onHashChange() {
        // Attempt to grab the location hash
        const locationHash = window.location.hash;
        // Next, grab the element ID from the hash...
        const id = locationHash.substring(1);
        // ... and for each reference,...
        this.references = this.references.map((reference) => {
            // ... check if it's the target,...
            const target = reference.modal.id === id;
            // ... if it's already open,...
            const open = reference.modal.classList.contains(this.classes.open);
            // ... and update its state
            reference.state = { target, open };
            return reference;
        });
        // Finally, toggle each reference modal open or close
        this.references.forEach(this.toggleModal.bind(this));
    }
    // Setup a modal reference
    setup(reference) {
        // Grab the modal
        const modal = reference.modal || reference;
        // Grab the given modal's overlay...
        const overlay =
            reference.overlay || modal.querySelector(this.selectors.overlay);
        // ... and close button,...
        const close =
            reference.close || modal.querySelector(this.selectors.close);
        // ... and use them for the modal's click events
        const onClick =
            reference.onClick || this.onClick.bind(this, overlay, close);
        modal[this.listener]('click', onClick);
        // Next, grab the modal's dialog box...
        const dialog =
            reference.dialog || modal.querySelector(this.selectors.dialog);
        // ... and the dialog box's focusable descendants,...
        const focusables =
            reference.focusables || getFocusableDescendants(dialog);
        // ... and for each focusable descendant, remove it from the tab order
        focusables.forEach((focusable) => toggleTabOrder(!this.run, focusable));
        // Finally, store a reference of the modal and its relevent elements
        if (this.run)
            this.references.push({
                modal,
                overlay,
                close,
                onClick,
                dialog,
                focusables
            });
    }
    // Mount/unmount the modal functionality
    mount(run) {
        super.mount(run);
        // Grab all the modals from the page...
        const modals = this.references.length
            ? this.references
            : Array.from(document.querySelectorAll(this.selectors.modal));
        // ... and set them up
        modals.forEach(this.setup);
        // If no modal references exist, do nothing else
        if (!this.references.length) return;
        // If running, immediately toggle the modals based on the current hash
        if (run) this.onHashChange();
        // Bind/unbind to the window's hash change...
        window[this.listener]('hashchange', this.onHashChange);
        // ... and document's key down events
        document[this.listener]('keydown', this.onKeyDown);
        // Finally, if unmounting, reset the references
        if (!run) this.references = [];
    }
}
                            
                            
                        The modal component is a hidden dialog box that only opens when triggered.
The modal is comprised of 4 main elements:
div.cwf-modal, encapsulates all modal pieces and indicates if the modal is visible or hidden using an aria-hidden attribute.div.cwf-modal__overlay, spans the entirety of the screen when the modal is opened, tints all content under it to make the dialog stand out, and adds or removes the modal from the tabindex using a tabindex attribute.button.cwf-modal__close, closes the modal and is always located at the top-right of the viewport.div.cwf-modal__dialog, contains the content and is the central focal point of the modal.The modal will open when the location hash changes to the ID of the wrapping modal element. This is usually triggered by adding a link to the modal on the page, but may be tapped into using custom JS or other means.
Once the modal is open, it will automatically focus the dialog box. The focus will be trapped within the modal until closed, cycling through the dialog, its focusable elements, and the close button.
There are 3 ways to close the modal:
Modals are implemented in T4 as the “Modal” plugin, meaning its classes are .plugin- prefixed instead of .cwf- prefixed.
This plugin can be used within the global “Site-Feature”, “Site-Sidebar”, and “Site-Footer” sections to have it displayed globally within the feature, sidebar, and footer areas respectively.
In the “Injectors”* or “Name” field of the “Modal” plugin, the following injectors can be used:
id:{custom_id} - Overrides the default, T4 ID of the modal with a custom ID.class:{custom_classes} - Adds custom classes to the modal.style:{custom_styles} - Adds custom styles to a style attribute of the modal.before:{custom_html} - Adds custom HTML before the modal.after:{custom_html} - Adds custom HTML after the modal.* These features are only supported on T41.