Modal/dialog window.
See smashingmagazine.com article and nathansmith/cta-modal.
See substrate-system.github.io/modal.
npm i -S @substrate-system/dialogNote
You should prevent the flash of undefined custom elements, because the modal content should be hidden from the user until it is opened. See abeautifulsite.net.
This is how I do it in the example page:
import { ModalWindow } from '@substrate-system/dialog'
(async () => {
await Promise.race([
// Load all custom elements
Promise.allSettled([
customElements.whenDefined(ModalWindow.TAG), // modal-window
]),
// Resolve after two seconds
new Promise(resolve => setTimeout(resolve, 2000))
])
// Remove the class, showing the page content
document.body.classList.remove('reduce-fouce')
})()And the HTML has a class reduce-fouce:
<body class="reduce-fouce">The CSS:
body {
&.reduce-fouce {
opacity: 0;
}
}If you are using a bundler:
import '@substrate-system/dialog/css'Or via CSS imports, for example with lightningcss:
@import url("@substrate-system/dialog/css");Customize the CSS with some variables.
:root {
/* Overlay */
--modal-overlay-z-index: 100000;
--modal-overlay-background-color: rgba(0, 0, 0, 0.5);
--modal-overlay-padding-top: 20px;
--modal-overlay-padding-left: 20px;
--modal-overlay-padding-right: 20px;
--modal-overlay-padding-bottom: 20px;
/* Dialog */
--modal-dialog-background-color: #fff;
--modal-dialog-border-radius: 0;
--modal-dialog-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.5);
--modal-dialog-padding: 1rem;
--modal-dialog-width: 500px;
/* Close button */
--modal-close-color: #fff;
--modal-close-background-color: #000;
--modal-close-border-radius: 50%;
--modal-close-box-shadow: 0 0 0 1px #fff;
--modal-close-display: block;
--modal-close-font-family: 'Arial', sans-serif;
--modal-close-font-size: 23px;
--modal-close-line-height: 26px;
--modal-close-width: 26px;
/* Close button hover/focus */
--modal-close-color-hover: #000;
--modal-close-background-color-hover: #fff;
--modal-close-box-shadow-hover: 0 0 0 1px #000;
}Just import. This calls the global function window.customElements.define.
import '@substrate-system/dialog'Then use the tag in HTML:
<button id="open-modal" type="button">Open modal</button>
<modal-window id="my-modal">
<h2>Modal Title</h2>
<p>This is the modal content.</p>
<p>Click the X, press Escape, or click outside to close.</p>
</modal-window>Open/close via JavaScript:
const modal = document.getElementById('my-modal')
document.getElementById('open-modal').addEventListener('click', () => {
modal.open()
})First copy the file to a location accessible to your web server.
cp ./node_modules/@substrate-system/dialog/dist/index.min.js ./public/dialog.jsThen link to the file in HTML
<body>
<p>...content...</p>
<script type="module" src="/dialog.js"></script>
</body>Controls whether the modal is open. Set to "true" to open, "false" or remove the attribute to close.
modal.setAttribute('active', 'true') // open
modal.setAttribute('active', 'false') // close
modal.removeAttribute('active') // closeSet to "false" to prevent the modal from being closed via the close button, Escape key, or clicking outside. You must close it programmatically. Defaults to true.
<modal-window closable="false">
<h2>Unclosable Modal</h2>
<p>This modal cannot be closed with the X button, Escape key, or clicking outside.</p>
<button id="close-btn" type="button">Close this modal</button>
</modal-window>document.getElementById('close-btn').addEventListener('click', () => {
modal.close()
})Hides the close button icon. Useful when you want to provide your own close UI.
<modal-window no-icon>
<header>
<button type="button" id="cancel">Cancel</button>
<h3>Edit profile</h3>
<button type="button" id="save">Save</button>
</header>
<div>...form content...</div>
</modal-window>Controls whether open/close animations are used. Set to "false" to disable. Defaults to true. Animations also respect prefers-reduced-motion.
<modal-window animated="false">
<p>No animation</p>
</modal-window>When set to "true", clicking outside the modal does not close it. The Escape key and close button still work (unless closable="false").
<modal-window static="true">
<p>Click outside won't close this</p>
</modal-window>When present, clicking the backdrop does not close the modal. Unlike static, this is a boolean attribute (no value needed). The Escape key and close button still work.
<modal-window noclick>
<p>Clicking the backdrop won't close this</p>
</modal-window>Sets the title/aria-label for the close button. Defaults to "Close".
<modal-window close="Dismiss">
<p>Close button will have title "Dismiss"</p>
</modal-window>Opens the modal and focuses it.
const modal = document.querySelector('modal-window')
modal.open()Closes the modal and returns focus to the previously focused element.
modal.close()Things handled by this library:
role="dialog"andaria-modal="true"on the dialog- Focus trapping (Tab cycles within modal)
- Escape key closes the modal (when
closable) - Focus returns to the trigger element on close
- Close button has
aria-label - Respects
prefers-reduced-motion
The component extracts text from the first heading (h1-h6) to use as the
dialog's aria-label. Always include a descriptive heading:
<!-- Good: heading text becomes the aria-label -->
<modal-window>
<h2>Edit Profile</h2>
<p>Update your information below.</p>
</modal-window>
<!-- Avoid: no heading results in aria-label="modal" -->
<modal-window>
<p>Some content without a heading...</p>
</modal-window>For modals with important supplementary text (like warnings), you can
add aria-describedby. This library will handle an aria-describedby attribute
correctly, meaning that it will be forwarded to the correct element.
<modal-window aria-describedby="delete-warning" id="confirm-delete">
<h2>Delete Account</h2>
<p id="delete-warning">
This action cannot be undone.
</p>
<button type="button">Cancel</button>
<button type="button">Delete</button>
</modal-window>Thanks @nathansmith and Smashing Magazine for publishing this originally.