Этот шаблон показывает, как создавать адаптивные к цвету, отзывчивые и доступные мини- и мегамодальные окна с помощью элемента <dialog>
.
Полная статья · Видео на YouTube · Источник на GitHub
HTML
<dialog id="MegaDialog" inert loading modal-mode="mega"> <form method="dialog"> <header> <section class="icon-headline"> <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24"> <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="8.5" cy="7" r="4"></circle> <line x1="20" y1="8" x2="20" y2="14"></line> <line x1="23" y1="11" x2="17" y2="11"></line> </svg> <h3>New User</h3> </section> <!-- TODO: Devsite - Removed inline handlers --> <!-- <button onclick="this.closest('dialog').close('close')" type="button" title="Close dialog"> --> <title>Close dialog icon</title> <svg width="24" height="24" viewBox="0 0 24 24"> <line x1="18" y1="6" x2="6" y2="18"/> <line x1="6" y1="6" x2="18" y2="18"/> </svg> </button> </header> <article> <section class="labelled-input"> <label for="userimage">Upload an image</label> <input id="userimage" name="userimage" type="file"> </section> <small><b>*</b> Maximum upload 1mb</small> </article> <footer> <menu> <button type="reset" value="clear">Clear</button> </menu> <menu> <!-- TODO: Devsite - Removed inline handlers --> <!-- <button autofocus type="button" onclick="this.closest('dialog').close('cancel')">Cancel</button> --> <button type="submit" value="confirm">Confirm</button> </menu> </footer> </form> </dialog> <dialog id="MiniDialog" inert loading modal-mode="mini"> <form method="dialog"> <article> <section class="warning-message"> <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" > <title>A warning icon</title> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path> <line x1="12" y1="9" x2="12" y2="13"></line> <line x1="12" y1="17" x2="12.01" y2="17"></line> </svg> <p>Are you sure you want to remove this user?</p> </section> </article> <footer> <menu> <!-- TODO: Devsite - Removed inline handlers --> <!-- <button autofocus type="button" onclick="this.closest('dialog').close('cancel')">Cancel</button> --> <button type="submit" value="confirm">Confirm</button> </menu> </footer> </form> </dialog>
CSS
@import "https://unpkg.com/open-props"; @import "https://unpkg.com/open-props/normalize.min.css"; html:has(dialog[open][modal-mode="mega"]) { overflow: hidden; } dialog { display: grid; background: var(--surface-2); color: var(--text-1); max-inline-size: min(90vw, var(--size-content-3)); max-block-size: min(80vh, 100%); max-block-size: min(80dvb, 100%); margin: auto; padding: 0; position: fixed; inset: 0; border-radius: var(--radius-3); box-shadow: var(--shadow-6); z-index: var(--layer-important); overflow: hidden; transition: opacity .5s var(--ease-3); @media (--motionOK) { animation: var(--animation-scale-down) forwards; animation-timing-function: var(--ease-squish-3); } @media (--OSdark) { border-block-start: var(--border-size-1) solid var(--surface-3); } @media (--md-n-below) { &[modal-mode="mega"] { margin-block-end: 0; border-end-end-radius: 0; border-end-start-radius: 0; @media (--motionOK) { animation: var(--animation-slide-out-down) forwards; animation-timing-function: var(--ease-squish-2); } } } &:not([open]) { pointer-events: none; opacity: 0; } &[modal-mode="mega"]::backdrop { backdrop-filter: blur(25px); } &[modal-mode="mini"]::backdrop { backdrop-filter: none; } &::backdrop { transition: backdrop-filter .5s ease; } &[loading] { visibility: hidden; } &[open] { @media (--motionOK) { animation: var(--animation-slide-in-up) forwards; } } & > form { display: grid; grid-template-rows: auto 1fr auto; align-items: start; max-block-size: 80vh; max-block-size: 80dvb; & > article { overflow-y: auto; max-block-size: 100%; /* safari */ overscroll-behavior-y: contain; display: grid; justify-items: flex-start; gap: var(--size-3); box-shadow: var(--shadow-2); z-index: var(--layer-1); padding-inline: var(--size-5); padding-block: var(--size-3); @media (--OSlight) { background: var(--surface-1); &::-webkit-scrollbar { background: var(--surface-1); } } @media (--OSdark) { border-block-start: var(--border-size-1) solid var(--surface-3); } } & > header { display: flex; gap: var(--size-3); justify-content: space-between; align-items: flex-start; padding-block: var(--size-3); padding-inline: var(--size-5); & > button { border-radius: var(--radius-round); padding: .75ch; aspect-ratio: 1; flex-shrink: 0; place-items: center; stroke: currentColor; stroke-width: 3px; } } & > footer { display: flex; flex-wrap: wrap; gap: var(--size-3); justify-content: space-between; align-items: flex-start; padding-inline: var(--size-5); padding-block: var(--size-3); & > menu { display: flex; flex-wrap: wrap; gap: var(--size-3); padding-inline-start: 0; &:only-child { margin-inline-start: auto; } @media (max-width: 410px) { & button[type="reset"] { display: none; } } } } & > :is(header, footer) { background-color: var(--surface-2); @media (--OSdark) { background-color: var(--surface-1); } } } }
JS
// Custom events to be added to dialog element: const dialogClosingEvent = new Event('closing'); const dialogClosedEvent = new Event('closed'); const dialogOpeningEvent = new Event('opening'); const dialogOpenedEvent = new Event('opened'); const dialogRemovedEvent = new Event('removed'); // Track opening: const dialogAttrObserver = new MutationObserver((mutations, observer) => { mutations.forEach(async mutation => { if (mutation.attributeName === 'open') { const dialog = mutation.target; const isOpen = dialog.hasAttribute('open'); if (!isOpen) { return; } dialog.removeAttribute('inert'); // Set focus: const focusTarget = dialog.querySelector('[autofocus]'); focusTarget ? focusTarget.focus() : dialog.querySelector('button').focus(); dialog.dispatchEvent(dialogOpeningEvent); await animationsComplete(dialog); dialog.dispatchEvent(dialogOpenedEvent); } }); }); // Track deletion: const dialogDeleteObserver = new MutationObserver((mutations, observer) => { mutations.forEach(mutation => { mutation.removedNodes.forEach(removedNode => { if (removedNode.nodeName === 'DIALOG') { removedNode.removeEventListener('click', lightDismiss); removedNode.removeEventListener('close', dialogClose); removedNode.dispatchEvent(dialogRemovedEvent); } }); }); }); // Wait for all dialog animations to complete their promises: const animationsComplete = element => Promise.allSettled( element.getAnimations().map(animation => animation.finished)); // Click outside the dialog handler: const lightDismiss = ({target: dialog}) => { if (dialog.nodeName === 'DIALOG') { dialog.close('dismiss'); } } const dialogClose = async ({target: dialog}) => { dialog.setAttribute('inert', ''); dialog.dispatchEvent(dialogClosingEvent); await animationsComplete(dialog); dialog.dispatchEvent(dialogClosedEvent); } // Page load dialogs setup: export default async function (dialog) { dialog.addEventListener('click', lightDismiss); dialog.addEventListener('close', dialogClose); dialogAttrObserver.observe(dialog, { attributes: true }); dialogDeleteObserver.observe(document.body, { attributes: false, subtree: false, childList: true }); // Remove loading attribute and prevent page load @keyframes playing: await animationsComplete(dialog); dialog.removeAttribute('loading'); }