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

<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>

       
@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);
     
}
   
}
 
}
}
       

       
// 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');
}