cssAudio - Activefile-genericCSS - ActiveGeneric - ActiveHTML - ActiveImage - ActiveJS - ActiveSVG - ActiveText - Activefile-genericVideo - ActiveLovehtmlicon-new-collectionicon-personicon-teamlog-outoctocatpop-outspinnerstartv

Pen Settings

CSS Base

Vendor Prefixing

Add External CSS

These stylesheets will be added in this order and before the code you write in the CSS editor. You can also add another Pen here, and it will pull the CSS from it. Try typing "font" or "ribbon" below.

Quick-add: + add another resource

Add External JavaScript

These scripts will run in this order and before the code in the JavaScript editor. You can also link to another Pen here, and it will run the JavaScript from it. Also try typing the name of any popular library.

Quick-add: + add another resource

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

            
              <div data-launchy="I like ๐ŸŒฎ" data-launchy-text="Launchy! ๐Ÿš€" data-launchy-title="Boom! ๐Ÿ’ฅ">
    <p>This modal window is fully accessible!</p>
    <p>Notice when you <code>Tab</code> or <code>Shift+Tab</code>, keyboard focus remains within the modal window. ๐Ÿ’ช</p>
    <p>To close the window press <code>Esc</code>, click the close button, or the modal overlay. Up to you, really.</p>
    <hr>
    <ul>
        <li>Check the project code on <a href="https://github.com/svinkle/launchy">GitHub</a>.</li>
        <li>Made with ๐Ÿ’– by <a href="https://twitter.com/svinkle">@svinkle</a>.</li>
    </ul>
    <a href="#" class="launchy__custom-control" data-launchy-close="๐Ÿ”ฅ">Ok! ๐Ÿ‘</a>
</div>
            
          
!
            
              /*** Vars ***/
// Colors
$black: Black;
$dark-purple: RebeccaPurple;
$light-gray: Silver;
$light-purple: Plum;
$off-black: #333;
$off-white: GhostWhite;
$overlay-bg: rgba(0, 0, 0, .5);
$purple: Purple;
$white: White;

// Typography
$main-font: 'Lobster', sans-serif;
$logo-font: 'Oxygen', sans-serif;

// Media queries
$break-point: 35rem;

/*** Launchy ***/
.js {
    [data-launchy] {
        display: none;
    }
}

.launchy__window {
    background-color: $off-white;
    border: transparent solid 1px;
    border-radius: 3px;
    bottom: 1rem;
    box-shadow: 1px 1px 10px $black;
    display: none;
    left: 1rem;
    padding: 1rem;
    position: absolute;
    right: 1rem;
    top: 1rem;
    z-index: 9999;

    @media (min-width: $break-point) {
        bottom: auto;
        left: 50%;
        max-width: 30rem;
        padding: 2rem;
        right: auto;
        top: 50%;
        transform: translate(-50%, -50%);
    }

    &--is-visible {
        display: block;

        [data-launchy] {
            display: block;
        }
    }
}

.launchy__content {
    position: relative;
}

.launchy__overlay {
    background-color: $black;
    background-color: $overlay-bg;
    bottom: 0;
    display: none;
    left: 0;
    position: fixed;
    right: 0;
    top: 0;
    z-index: 9998;

    &--is-visible {
        display: block;
    }
}

.launchy__close-link {
    background-color: $purple;
    border-radius: 3px;
    color: $white;
    display: inline-block;
    font-size: 1.25rem;
    padding: .15rem .5rem;
    position: absolute;
    right: 0;
    text-decoration: none;
    top: 0;

    @media (min-width: $break-point) {
        right: -1rem;
        top: -1rem;
    }

    &:focus,
    &:hover {
        background-color: $dark-purple;
        color: $white;
    }
}

.launchy__title {
    font-family: $main-font;
    font-size: 2.5em;
    margin: 0;
}

.launchy__launch-link {
    color: $white;
    display: inline-block;
    font-family: $main-font;
    font-size: 3.5em;
    font-weight: 700;
    text-decoration: none;
    text-shadow: 1px 1px 2px $black;

    &:focus,
    &:hover {
        color: $white;
        text-decoration: underline;
    }
}

.launchy__custom-control {
    background-color: $purple;
    border-radius: 3px;
    color: $white;
    display: inline-block;
    padding: .5rem;
    text-decoration: none;

    &:focus,
    &:hover {
        background-color: $dark-purple;
        color: $white;
    }
}

/*** Extra for the demo ***/
* {
    box-sizing: border-box;
}

body {
    align-items: center;
    background-color: $off-black;
    background-image: radial-gradient(circle at center, $light-purple, $purple);
    display: flex;
    font-family: $logo-font;
    min-height: 100vh;
    justify-content: center;
    position: relative;
}

a {
    color: $purple;
    text-decoration-skip: ink;

    &:focus,
    &:hover {
        color: $dark-purple;
    }
}

hr {
    border: solid transparent 1px;
    border-top: solid $light-gray 1px;
    height: 0;
    margin: 1.5em 0;
    width: 100%;
}

button {
    background-color: $purple;
    border: 0;
    border-radius: 3px;
    color: $white;
    font-family: $logo-font;
    font-weight: 700;
    padding: .5em;

    &:focus,
    &:hover {
        background-color: $dark-purple;
    }
}
            
          
!
            
              /**
 * Launchy! ๐Ÿš€ โ€” An Accessible Modal Window
 *
 * Features include:
 * - On launch, shift focus to the modal window container
 * - The modal window is described by the modal heading
 * - Trap keyboard focus within the modal when active/visible
 * - Close the window on `esc` key press
 * - Close the window on overlay `click`
 * - Set keyboard focus back to the launcher element on window close
 * - Transparent border for Windows High Contrast themes
 *
 * Check out the GitHub repo for more information: https://github.com/svinkle/launchy
 *
 * @author Scott Vinkle <svinkle@gmail.com>
 * @version 0.8.0
 * @license MIT
 */

// HTML elements
const htmlElements = {
    launchModal: 'a',
    closeModal: 'a',
    modalWindow: 'div',
    modalContent: 'div',
    modalOverlay: 'div',
    modalTitle: 'h2'
};

// CSS classes
const classes = {
    modalLaunchLink: 'launchy__launch-link',
    modalCloseLink: 'launchy__close-link',
    modalWindow: 'launchy__window',
    modalContent: 'launchy__content',
    modalOverlay: 'launchy__overlay',
    modalTitle: 'launchy__title',
    modalWindowIsVisible: 'launchy__window--is-visible',
    modalOverlayIsVisible: 'launchy__overlay--is-visible'
};

// Data attributes
const data = {
    launchyAriaHidden: 'data-launchy-aria-hidden',
    launchyFocusable: 'data-launchy-focusable',
    launchyTabIndex: 'data-launchy-tabindex',
    launchyText: 'data-launchy-text',
    launchyTitle: 'data-launchy-title',
    launchyCustom: {
        close: 'data-launchy-close',
        refocus: 'data-launchy-refocus'
    }
};

// Keys
const keysCodes = {
    'Escape': 27
};

// Selectors
const selectors = {
    launchyElements: '[data-launchy]',
    launchyControl: 'launchy-control-',
    launchyDialog: 'launchy-dialog-',
    launchyCloseControl: 'launchy-close-control-',
    modalOverlay: 'modal-overlay-',
    modalTitle: 'modal-title-'
};

// Strings
const strings = {
    modalClose: 'Close modal window!',
    modalCloseHTML: '<span aria-hidden="true">&times;</span>',
    modalError: 'Launchy container must have a `data-launchy-text` attribute!',
    modalErrorEmpty: 'Launchy container `data-launchy-text` attribute cannot be empty!',
    modalWarning: 'Launchy container should have a `data-launchy-title` attribute, or be sure to supply your own heading! (Prefereably an `<h2>`.)',
    refocusElemNotFound: 'Refocus element not found!'
};

// Unique identifier
let launchyId = 0;

class Launchy {
    constructor(params) {

        // https://www.npmjs.com/package/focusable
        this.focusable = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable], audio[controls], video[controls]';

        // Unique identifier for each instance
        this.launchyId = launchyId;

        // Flags and other objects to be used later
        this.hasTitle = params.title ? true : false;
        this.modalIsVisible = false;
        this.activeElement = null;
        this.shiftKeyIsPressed = false;
        this.allFocusable = null;
        this.firstFocusable = null;
        this.lastFocusable = null;
        this.domFocusable = null;

        // Setup all the things
        this.prepareFocusable();
        this.createElements(params);
        this.insertElements(params);
        this.setupEventListeners();

        // Increment identifier
        launchyId++;
    }

    /**
     * Add a data attribute on all existing focusable elements. Used in
     * `modalHide()` and `modalShow()` to make elements "inert" -- prevent
     * screen readers from reaching these elements when using other means
     * of navigation (arrow keys, for example.)
     *
     * @return {null}
     */
    prepareFocusable() {

        // Select all focusable elements in the DOM
        this.domFocusable = document.querySelectorAll(this.focusable);

        // For each focusable element in the DOM, set the data attribute
        for (const domElement of Array.from(this.domFocusable)) {
            let addAttributes = false;

            // Check to see if the element already has `tabindex="-1"`
            if (!domElement.hasAttribute('tabindex') || domElement.getAttribute('tabindex') !== '-1') {
                domElement.setAttribute(data.launchyTabIndex, true);
                addAttributes = true;
            }

            // Check to see if the element already has `aria-hidden="true"`
            if (!domElement.hasAttribute('aria-hidden') || domElement.getAttribute('aria-hidden') !== 'true') {
                domElement.setAttribute(data.launchyAriaHidden, true);
                addAttributes = true;
            }

            // Only add this element to the set if the above conditions are met
            if (addAttributes) {
                domElement.setAttribute(data.launchyFocusable, true);
            }
        }
    }

    /**
     * Create all the required elements for Launchy to function.
     *
     * @param {Object} params Instance parameters
     * @return {null}
     */
    createElements(params) {

        // Launch control
        this.launchControl = document.createElement(htmlElements.launchModal);
        this.launchControl.id = `${selectors.launchyControl}${this.launchyId}`;
        this.launchControl.href = `#${selectors.launchyDialog}${this.launchyId}`;
        this.launchControl.classList.add(classes.modalLaunchLink);
        this.launchControl.setAttribute('aria-haspopup', 'dialog');
        this.launchControl.textContent = params.text;

        // Close control
        this.closeControl = document.createElement(htmlElements.closeModal);
        this.closeControl.id = `${selectors.launchyCloseControl}${this.launchyId}`;
        this.closeControl.href = `#${selectors.launchyControl}${this.launchyId}`;
        this.closeControl.classList.add(classes.modalCloseLink);
        this.closeControl.setAttribute('aria-label', strings.modalClose);
        this.closeControl.innerHTML = strings.modalCloseHTML;

        // Modal window
        this.modalWindow = document.createElement(htmlElements.modalWindow);
        this.modalWindow.id = `${selectors.launchyDialog}${this.launchyId}`;
        this.modalWindow.classList.add(classes.modalWindow);
        this.modalWindow.setAttribute('tabindex', -1);
        this.modalWindow.setAttribute('role', 'dialog');
        this.modalWindow.setAttribute('aria-modal', true);

        if (this.hasTitle) {
            this.modalWindow.setAttribute('aria-labelledby', `${selectors.modalTitle}${this.launchyId}`);
        }

        // Modal overlay
        this.modalOverlay = document.createElement(htmlElements.modalOverlay);
        this.modalOverlay.id = `${selectors.modalOverlay}${this.launchyId}`;
        this.modalOverlay.classList.add(classes.modalOverlay);
        this.modalOverlay.setAttribute('tabindex', 0);

        // Modal content
        this.modalContent = document.createElement(htmlElements.modalContent);
        this.modalContent.classList.add(classes.modalContent);

        // Modal title
        if (this.hasTitle) {
            this.modalTitle = document.createElement(htmlElements.modalTitle);
            this.modalTitle.id = `${selectors.modalTitle}${this.launchyId}`;
            this.modalTitle.classList.add(classes.modalTitle);
            this.modalTitle.textContent = params.title;
        }
    }

    /**
     * Insert Launchy elements into the DOM.
     *
     * @param {Object} params instance parameters
     * @return {null}
     */
    insertElements(params) {

        // Select all focusable elements in the modal content
        const domFocusable = params.target.querySelectorAll(this.focusable);

        // Launch control
        params.target.parentNode.insertBefore(this.launchControl, params.target);

        // Modal window
        params.target.parentNode.insertBefore(this.modalWindow, params.target);

        // Modal content container
        this.modalWindow.appendChild(this.modalContent);

        // Close control
        this.modalContent.appendChild(this.closeControl);

        // Modal title
        if (this.hasTitle) {
            this.modalContent.appendChild(this.modalTitle);
        }

        // Move the content within the modal container
        this.modalContent.appendChild(params.target);

        // Remove `data-launchy-focusable` from any elements within the
        // modal content -- we don't want to make these inert
        for (const domElement of Array.from(domFocusable)) {
            domElement.removeAttribute(data.launchyAriaHidden);
            domElement.removeAttribute(data.launchyFocusable);
            domElement.removeAttribute(data.launchyTabIndex);
        }

        // Overlay
        document.body.appendChild(this.modalOverlay);
    }

    /**
     * Create event listeners for Launchy functionality.
     *
     * @return {null}
     */
    setupEventListeners() {
        
        // Gather any custom close or refocus controls
        const closeControls = this.modalContent.querySelectorAll(`[${data.launchyCustom.close}]`);
        const refocusControls = this.modalContent.querySelectorAll(`[${data.launchyCustom.refocus}]`);

        // Show the modal window on the launcher element `click` event
        this.launchControl.addEventListener('click', this.showModal.bind(this), false);

        // Hide the modal window on close button or overlay `click` event
        this.closeControl.addEventListener('click', this.hideModal.bind(this), false);
        this.modalOverlay.addEventListener('click', this.hideModal.bind(this), false);

        // Trap the keyboard focus within modal window on the document
        // `focus` event. Notice the use of the `useCapture` flag set to `true`; this
        // indicates the event will be dispatched to the listener before any event
        // target in the DOM:
        // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
        document.addEventListener('focus', this.trapFocus.bind(this), true);

        // Check for `esc` key press on the document `keydown` event
        document.addEventListener('keydown', this.checkEsc.bind(this), false);
        
        // Add event listener for all custom close controls
        for (const closeControl of Array.from(closeControls)) {
            closeControl.addEventListener('click', this.hideModal.bind(this), false);
        }

        // Add event listener for all custom refocus controls
        for (const refocusControl of Array.from(refocusControls)) {
            refocusControl.addEventListener('click', this.hideModalRefocus.bind(this), false);
        }
    }

    /**
     * Show the modal window.
     *
     * @param {Object} e The event object
     * @return {null}
     */
    showModal(e) {
        e.preventDefault();

        // Cache the last active element
        this.activeElement = document.activeElement;

        // Set visible flag as `true`
        this.modalIsVisible = true;

        // Set the focusable objects, first and last, within the modal window
        this.allFocusable = this.modalWindow.querySelectorAll(this.focusable);
        this.firstFocusable = this.allFocusable[0];
        this.lastFocusable = this.allFocusable[this.allFocusable.length - 1];

        // Add the `active` classes and set `aria-hidden` to `false`
        this.modalWindow.classList.add(classes.modalWindowIsVisible);
        this.modalOverlay.classList.add(classes.modalOverlayIsVisible);
        this.modalWindow.setAttribute('aria-hidden', false);

        // Set focusable elements as "inert"
        this.inertElements(true);

        // Shift keyboard focus to the modal window container
        this.modalWindow.focus();
    };

    /**
     * Hide the modal window.
     *
     * @param {Object} e The event object
     * @return {null}
     */
    hideModal(e) {
        e.preventDefault();

        // Set visible flag to `false`
        this.modalIsVisible = false;

        // Reset the focusable objects
        this.allFocusable = null;
        this.firstFocusable = null;
        this.lastFocusable = null;

        // Remove the `active` classes and set `aria-hidden` to `true`
        this.modalWindow.classList.remove(classes.modalWindowIsVisible);
        this.modalOverlay.classList.remove(classes.modalOverlayIsVisible);
        this.modalWindow.setAttribute('aria-hidden', true);

        // Remove "inert" state for focusable elements
        this.inertElements(false);

        // Set focus to the previous active element
        this.activeElement.focus();
    };

    /**
     * Trap keyboard focus within the modal window.
     *
     * @param {Object} e The event object
     * @return {null}
     */
    trapFocus(e) {

        // If the modal is currently visible _and_ the currently focused element
        // is _not_ within the modal windowโ€ฆ
        if (this.modalIsVisible && !this.modalWindow.contains(e.target)) {

            // Stop the event from bubbling any further up into the DOM:
            // https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation
            e.stopPropagation();

            // If the user is moving forward, focus on the first element,
            // otherwise, the `shift` key is pressed; focus on the last element
            this.shiftKeyIsPressed ? this.lastFocusable.focus() : this.firstFocusable.focus();
        }
    };

    /**
     * Check if the `esc` key has been pressed.
     *
     * @param {Object} e The event object
     * @return {null}
     */
    checkEsc(e) {
        if (this.modalIsVisible) {

            // Cache the `shift` key state
            this.shiftKeyIsPressed = e.shiftKey;

            // Hide the modal window on `esc` key press
            if (e.keyCode === keysCodes.Escape) {
                this.hideModal(e);
            }
        }
    };
    
    /**
     * Send focus to the specified element `id` on custom refocus element click
     *
     * @param {Object} e The event object
     * @return {null}
     */
    hideModalRefocus(e) {
        const refocusId = e.target.getAttribute(data.launchyCustom.refocus);
        const refocusElem = document.querySelector(`#${refocusId}`);

        // Throw an error if the refocus element is not found
        if (refocusElem == null) {
            throw Error(`${strings.refocusElemNotFound}: #${refocusId}`);
            return;
        }

        // Hide the modal
        this.hideModal(e);

        // Send focus to the specified element
        refocusElem.focus();
    };

    /**
     * Set all existing focusable elements as "inert" -- hide from screen
     * readers in order to keep focus trapped within modal when using other
     * forms of keyboard navigation (other than tab).
     *
     * @param {Boolean} inert Flag to set elements "inert" state
     * @return {null}
     */
    inertElements(inert) {

        // Select all `data-launchy-focusable` elements
        const domFocusable = document.querySelectorAll(`[${data.launchyFocusable}]`);

        for (const domElement of Array.from(domFocusable)) {
            if (inert) {

                // If the element has the launchy aria-hidden data attribute,
                // hide from screen readers
                if (domElement.hasAttribute(data.launchyAriaHidden)) {
                    domElement.setAttribute('aria-hidden', true);
                }

                // If the element has the launchy tabindex data attribute,
                // remove element from tab order
                if (domElement.hasAttribute(data.launchyTabIndex)) {
                    domElement.setAttribute('tabindex', -1);
                }
            } else {

                // Ditto ๐Ÿ‘†, except remove the attributes to reset as focusable
                if (domElement.hasAttribute(data.launchyAriaHidden)) {
                    domElement.removeAttribute('aria-hidden', true);
                }

                if (domElement.hasAttribute(data.launchyTabIndex)) {
                    domElement.removeAttribute('tabindex', -1);
                }
            }
        }
    };
}

const init = () => {

    // Create instances per `data-launchy` elements found in the DOM
    const launchyElements = document.querySelectorAll(selectors.launchyElements);

    let launchyText = null,
        launchyTitle = null;

    for (const launchyElement of Array.from(launchyElements)) {
        launchyText = launchyElement.getAttribute(data.launchyText),
        launchyTitle = launchyElement.getAttribute(data.launchyTitle);

        // Throw an error if there's no launcher control text attribute
        if (!launchyText) {
            throw Error(strings.modalError);
            break;
        }

        // Throw an error if the launcher control text is empty
        if (launchyText.trim() === '') {
            throw Error(strings.modalErrorEmpty);
            break;
        }

        // Throw a warning if there's no heading title text
        if (!launchyTitle) {
            console.warn(strings.modalWarning);
        }

        // Params object to send to Launchy constructor
        const params = {
            target: launchyElement,
            text: launchyText,
            title: launchyTitle
        };

        // Create a new instance for each found in the DOM
        new Launchy(params);
    }
};

document.addEventListener('DOMContentLoaded', init, false);

            
          
!
999px
Close

Asset uploading is a PRO feature.

As a PRO member, you can drag-and-drop upload files here to use as resources. Images, Libraries, JSON data... anything you want. You can even edit them anytime, like any other code on CodePen.

Go PRO

Loading ..................

Console