Dialog

Questo pattern mostra come creare mini e megamodali adattabili al colore, adattabili e accessibili con l'elemento <dialog>.

Articolo completo · Video su YouTube · Fonte su 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'); }