MartyIX
MartyIX

Reputation: 28646

How to fade in a HTML5 dialog?

How can I fade in an HTML5 dialog? And by dialog I mean HTML5 <dialog> tag (http://demo.agektmr.com/dialog/).

I tried the following (http://jsfiddle.net/v6tbW/) but for some reason the transition does not work.

document.getElementById('myDialog').show(); // note that this is a method of <dialog>, this is not a jQuery method.
dialog {
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  border: solid;
  padding: 1em;
  background: white;
  color: black;

  width: -moz-fit-content;
  width: -webkit-fit-content;
  width: fit-content;

  height: -moz-fit-content;
  height: -webkit-fit-content;
  height: fit-content;

  visibility: hidden;
  opacity: 0;
  transition: visibility 10s linear 10s, opacity 10s linear;
}

dialog[open] {
  visibility: visible;
  opacity: 1;
  transition-delay: 0s;
}

.backdrop {
  position: fixed;
  background: rgba(0, 0, 0, 0.1);
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
<dialog id="myDialog">Test</dialog>

Upvotes: 23

Views: 16193

Answers (8)

wick3d
wick3d

Reputation: 672

Here's a working example of using css transition that you started with and proper jquery selector, that adds the "no-ninja" class to your DIV, on window load event:

http://jsfiddle.net/v6tbW/6/

$("#myDialog").addClass('no-ninja');
dialog {
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  border: solid;
  padding: 1em;
  background: red;
  color: black;
  dispaly: block;

  width: -moz-fit-content;
  width: -webkit-fit-content;
  width: fit-content;

  height: -moz-fit-content;
  height: -webkit-fit-content;
  height: fit-content;

  /*visibility:hidden;*/
  opacity: 0;
  -webkit-transition: opacity 10s linear;
}

dialog[open] {
  visibility: visible;
  opacity: 1;
  transition-delay: 0s;
}

.backdrop {
  position: fixed;
  background: rgba(0, 0, 0, 0.1);
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.no-ninja {
  opacity: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<dialog id="myDialog">
  Test
</dialog>

Upvotes: -2

Niklas
Niklas

Reputation: 1777

Since you're using jQuery. This is an easier approch:

http://jsfiddle.net/v6tbW/3/

$(function() {

  $('#myDialog').fadeIn(10000);

});
dialog {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  border: solid;
  padding: 1em;
  background: white;
  color: black;

  width: -moz-fit-content;
  width: -webkit-fit-content;
  width: fit-content;

  height: -moz-fit-content;
  height: -webkit-fit-content;
  height: fit-content;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<dialog id="myDialog">
  Test
</dialog>

Upvotes: -1

Carson
Carson

Reputation: 8028

You can consider using

dialog[open] {
    animation: myFadeIn 5.0s ease normal;
  }
  
@keyframes myFadeIn{
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

Eample

<style>
  /* 👇 Optional. change the background style. */
  dialog::backdrop {
    background-color: rgba(255, 128, 30, .75);
    backdrop-filter: blur(3px);
  }

  /* 👇 style1: fadeIn */
  dialog[open] {
    animation: myFadeIn 5.0s ease normal;
  }
  @keyframes myFadeIn{
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }

  /* 👇 style2: top2center */
  dialog#top2center[open] {
    // Find your favorite style from here: https://cubic-bezier.com/
    // animation: myTop2Center 3.0s ease normal;
    animation: myTop2Center 1.2s cubic-bezier(.33,1.44,.83,.22)
  }
  @keyframes myTop2Center{
    from {
      transform: translateY(-200%);
    }
    to {
      transform: translateY(0%);
    }
  }
</style>

<dialog>
  <header>FadeIn</header>
  <form>
    <button>Close</button>
  </form>
</dialog>

<dialog id="top2center">
  <header>Top2Center</header>
  <form>
    <button>Close</button>
  </form>
</dialog>

<script>
  document.querySelectorAll(`dialog`).forEach(dialogElem=>{
    const testName = dialogElem.querySelector(`header`).innerText
    const frag = document.createRange().createContextualFragment(`<button>${testName}</button><br>`)
    const showBtn = frag.querySelector(`button`)
    const closeBtn = dialogElem.querySelector(`button`)
    showBtn.onclick = () => dialogElem.showModal()
    closeBtn.onclick = () => dialogElem.close()
    dialogElem.querySelector(`form`).onsubmit = () => false // To stop submit event.
    document.body.append(frag)
  })
</script>

Upvotes: 9

herrstrietzel
herrstrietzel

Reputation: 17240

Update 2024: you still need a lot of tweaking

Expanding on Alexander Nenashev's answer pointing out severe accessibility issues (e.g focusable close buttons):

  • opening the dialog with a transition/animation requires to override the default display value to something other than none (e.g block, flex or grid) – this also introduces accessibility issues because elements like the close button are still focusable – we need e.g visibility:hidden to avoid this behavior
  • closing the dialog immediately toggles its display value to none – no chance to see any transition/animation – unless we add some delay
  • :backdrop pseudo element can't be animated in Firefox to this date

Neither the CSS animate property nor WAAPI JS interface can circumvent all these restriction.

Example1: Delay closed state

const showButton = document.getElementById("btnOpenModal");
const dialog = document.getElementById("dialog");

initDialog(dialog, showButton);

function initDialog(dialog, showButton) {
  let closeButton = dialog.querySelector(".dialog-btn-close");
  // get transition timings from computed style
  let style = getComputedStyle(dialog);
  let duration = parseFloat(style.getPropertyValue("transition-duration")) * 1000;
  let delay = parseFloat(style.getPropertyValue("transition-delay")) * 1000;

  // "Show the dialog" button opens the dialog modally
  showButton.addEventListener("click", () => {
    dialog.showModal();
    dialog.classList.add("dialog-open");
  });

  // "Close" button closes the dialog
  closeButton.addEventListener("click", () => {
    closeDialog()
  });


  function closeDialog() {
    dialog.classList.remove("dialog-open");

    // delay close to enable backdrop transition
    setTimeout(() => {
      dialog.close();
    }, duration + delay);
  }

}
dialog {
  --transition-delay: 0s;
  --transition-duration: 2s;
  display: block;
  visibility: hidden;
}

dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.8);
}


/* transitions */

dialog,
dialog::backdrop {
  transition: var(--transition-duration) var(--transition-delay);
  opacity: 0;
}

.dialog-open,
.dialog-open::backdrop {
  visibility: visible;
  opacity: 1;
}

.dialog-closed::backdrop {
  display: block;
  opacity: 0;
}

.dialog-open::backdrop {
  display: block;
  opacity: 1;
}


/** btns **/

.dialog-btn-close {
  right: 0;
  top: 0;
  font-size: 24px;
  appearance: none;
  border: none;
  background: none;
  cursor: pointer;
}
<h3>Try Tab navigation</h3>
<p>
  Text after before dialog
  <a href="#">Test link (focus test)</a>
</p>
<button id="btnOpenModal">Show the dialog</button>
<dialog id="dialog" class="dialog-enhanced dialog-closed">
  <button type="button" class="dialog-btn-close" title="close" aria-label="close dialog">&times;</button>
  <p>This modal dialog has a groovy backdrop!</p>
</dialog>

<p>
  Text after hidden dialog
  <a href="#">Test link (focus test)</a>
</p>

Basically, we add a delay via setTimeout() based on the CSS transition timing property values (hence the usage of computedStyle()) to enable the fade out transition. The above example works pretty fine on chromium based browsers and even on current iOS safari/webkit implementations but it doesn't work on Firefox.

Example 2: replace :backdrop pseudo element by :before

This approach also adds the common "click-on-overlay-to-close" functionality we're used to from other modal/lightbox libraries. It also improves cross-browser-compatibility by replacing the :backdrop pseudo element by a :before to get more advanced styling capabilities.

let dialogSelector = "[data-dialog]";
initDialogs(dialogSelector);

function initDialogs(dialogSelector = "[data-dialog]") {
  let dialogBtns = document.querySelectorAll(dialogSelector);
  if(!dialogBtns.length) return false;
  
  
  const closeDialog = (dialog, dialogWrap, duration, delay)=>{
    dialogWrap.classList.remove("dialog-active");

    // delay close to enable backdrop transition
    setTimeout(() => {
      dialog.close();
    }, (duration + delay));
  }
  
  dialogBtns.forEach( (dialogBtn) => {
    // query target dialog from button
    let dialog = document.querySelector(dialogBtn.dataset.dialog);
    // fallback take first dialog element
    dialog = dialog ? dialog : document.querySelector('dialog');

    // no dialog - exit
    if(!dialog) return false;
        
    /**
    * add close button if it doesn't exist 
    * (e.g if already added by previous/duplicate dialog targets)
    */
    let dialogBtnClose = dialog.querySelector('.dialog-btn-close');
    if(!dialogBtnClose){
      dialogBtnClose = document.createElement('button')
      dialogBtnClose.setAttribute('aria-label', 'Close dialog')
      dialogBtnClose.setAttribute('type', 'button')
      dialogBtnClose.classList.add('dialog-btn-close');
      dialogBtnClose.textContent = '×';
      dialog.insertBefore(dialogBtnClose, dialog.children[0] )
    }

    
    // wrap dialog to replace backface with :before pseudo
    let dialogWrap = dialog.closest('.dialog-wrap');
    if(!dialogWrap){
      dialogWrap = document.createElement('div');
      dialog.parentNode.insertBefore(dialogWrap, dialog);
      dialogWrap.classList.add('dialog-wrap');
      dialogWrap.append(dialog);
    }
    
    // get transition timings from computed style
    let style = getComputedStyle(dialog);
    let duration = parseFloat(style.getPropertyValue("transition-duration")) * 1000;
    let delay = parseFloat(style.getPropertyValue("transition-delay")) * 1000;

    // open dialog modally
    dialogBtn.addEventListener("click", (e) => {
      e.preventDefault();
      dialog.showModal();
      dialogWrap.classList.add("dialog-active");
    });

    
    // "Close" button closes the dialog
    dialogBtnClose.addEventListener("click", () => {
      closeDialog(dialog, dialogWrap, duration, delay);
    });

    // close on backdrop click
    dialog.addEventListener("click", (e) => {
      //get bounding box to close dialog when clicking outside dialog box
      let {
        left,
        top,
        right,
        bottom,
        width,
        height
      } = dialog.getBoundingClientRect();

      if (dialog.open) {
        let pt = { x: e.clientX, y: e.clientY };
        // is outsite bbox
        if (pt.x > right || pt.x < left || pt.y > bottom || pt.y < top) {
          closeDialog(dialog, dialogWrap, duration, delay);
        }
      }
    });
  });
}
body {
  font-family: "Fira Sans", "Open Sans", sans-serif;
}

:root {
  --transition-delay: 0s;
  --transition-duration: 0.5s;
}

dialog {
  position: absolute;
  margin-top: 25vh;
  width: 75%;
  border-radius: 0.3em;
  padding: 1.5em 0.5em 1em 0.5em;
  border: none;
  filter: drop-shadow(0.5em 0.5em 0.5em rgba(0, 0, 0, 0.4));
  visibility: hidden;
}

/* create backdrop replacement to enable transition */
.dialog-wrap::before {
  content: "";
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.7);
  pointer-events: none;
}

/* hide default backdrop */
dialog::backdrop {
  opacity: 0;
}

/* add transitions */
dialog,
.dialog-wrap::before {
  display: block;
  transition: var(--transition-duration) var(--transition-delay) opacity;
  opacity: 0;
}

.dialog-active dialog, .dialog-active::before {
  opacity: 1;
}
.dialog-active dialog {
  visibility: visible;
}

/** btns **/
.dialog-btn-close {
  position: absolute;
  right: 0;
  top: 0;
  font-size: 24px;
  line-height: 0;
  width: 1em;
  height: 1em;
  display: block;
  appearance: none;
  border: none;
  background: none;
  cursor: pointer;
}
<h1>Animated native HTML dialog element</h1>
<p><a href="#" data-dialog="#dialog-2" >Show Dialog 2</a></p>
<dialog id="dialog-1">
  <h3>Dialog 1</h3>
  <p>One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment.</p>
</dialog>

<dialog id="dialog-2">
  <h3>Dialog 2</h3>
 <p>His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked. "What's happened to me? " he thought. It wasn't a dream. His room, a proper human room although a little too small, lay peacefully between its four familiar walls.</p>
</dialog>

<!--specify dialog target via data attribute -->
<button id="btnOpenModal" data-dialog="#dialog-1">Show Dialog 1</button>
<button id="btnOpenModal2" data-dialog="#dialog-2">Show Dialog 2</button>

The above examples raise the question:

What's the point of native <dialog> elements?

Ideally, these native HTML elements should provide better semantics as well as better accessibility ... not to forget a more developer friendly approach. Unfortunately, the current implementations are not too impressive. If you're already deploying an advanced dialog/modal field library (optimized for accessibility) there is probably no reason to switch to the native <dialog> element as it requires JavaScript nonetheless (in contrast to the <details> element as a potential native replacements for accordions)

Upvotes: 1

PsiKai
PsiKai

Reputation: 1978

Fade in and out example

This example uses a combination of CSS animation and the Javascript Web Animations API to achieve a fade in and fade out without disrupting the intended display properties of the dialog.

On the fade in, use a CSS animation to give the dialog opacity and translate, or whichever effect is desired, by applying the animation to the dialog[open] selector.

On close, create a new Animation and reverse the original animation with a KeyFrameEffect. Play the animation and listen for the finish event, and call the modal close function inside the finish event handler.

const dialog = document.querySelector("dialog")
const open = document.querySelector("#open")
const close = document.querySelector("#close")

open.addEventListener("click", () => dialog.showModal())
close.addEventListener("click", handleClose)

function handleClose() {
  const keyFrame = new KeyframeEffect(
    dialog, 
    [{ translate: "0 -100%", opacity: "0" }], 
    { duration: 500, easing: "ease", direction: "normal" }
  )

  const animation = new Animation(keyFrame, document.timeline)
  animation.play()
  animation.onfinish = () => dialog.close()
}
dialog[open] {
  animation: modal-in 500ms forwards ease;
}

@keyframes modal-in {
  from {
    translate: 0 -100%;
    opacity: 0;
  }
}

dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.2)
}
<button id="open">Open dialog</button>

<dialog>
  <p>Animated open and close</p>
  <button id="close">Close dialog</button>
</dialog>

Side note

I was unable to transition or animate the ::backdrop in any way. That is the last piece I was unable to solve.

Upvotes: 5

Alexander Nenashev
Alexander Nenashev

Reputation: 22942

First I've tried King Friday's solution and found it faulty. The problem that if you change a closed dialog's default display:none you are actually just hide it and it responds to keyboard events:

dialog {
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
dialog[open] {
  opacity: 1;
  pointer-events: inherit;
}
dialog::backdrop {
  background-color: rgba(0,0,255, 0.2);
}
Press TAB, the focus will be on the show dialog button
<br>
Then press TAB again, the focus will be on the dialog close button, and its `keyup` listener will be triggered
<br>

<button id="$open" onclick="dialog.showModal()">show dialog</button>
<dialog id="dialog">
  <p>hi i'm a dialog!</p>
  <form method="dialog">
    <button onkeyup="alert('OOPS, the hidden dialog works!')">Close</button>
  </form>
</dialog>
<script>setTimeout(()=>$open.focus(),100)</script>

So to my answer:

A dialog transitions from display:none to display:block on the opening and that ruins transition. The idea was to add dialog { display:block; } but it turned out bad because the dialog is just hidden and reacts to the keyboard TAB navigation for example... So we need to keep a closed dialog display:none.

So a couple of solutions:

  1. You could use animation which I like since it's a pure CSS solution:
dialog[open] {
  animation: fadein 2s ease-in forwards;
}

@keyframes fadein{
  0%{
    opacity:0;
  }
  100%{
    opacity:1;
    background-color: green;
  }
}

const dialog = document.querySelector("dialog");

document.querySelector("#open-button").addEventListener("click", () => {
  dialog.showModal();
});
document.querySelector("#close-button").addEventListener("click", () => {
  dialog.close();
});
button {
  display: block;
}

dialog {
  position: absolute;
  top: 50px;
  margin: auto;
  padding: 0;
  width: 50%;
  height: 50%;
  background-color: red;
  opacity: 0;
}

dialog[open] {
  animation: fadein 2s ease-in forwards;
}

@keyframes fadein{
  0%{
    opacity:0;
  }
  100%{
    opacity:1;
    background-color: green;
  }
}
<button id="open-button">Open Dialog Element</button>

<dialog>
  <button id="close-button">Close Dialog Element</button>
</dialog>

  1. You could also add a CSS class after the opening with setTimeout to allow the DOM to be re-rendered and remove it on the closing with your transition left intact.
setTimeout(()=>dialog.classList.add('open'));
dialog.addEventListener('close', () => dialog.classList.remove('open'));

const dialog = document.querySelector("dialog");

dialog.addEventListener('close', () => dialog.classList.remove('open'));

document.querySelector("#open-button").addEventListener("click", () => {
  dialog.showModal();
  setTimeout(()=>dialog.classList.add('open'));
});

document.querySelector("#close-button").addEventListener("click", () => {
  dialog.close();
});
button {
  display: block;
}

dialog {
  position: absolute;
  top: 50px;
  margin: auto;
  padding: 0;
  width: 50%;
  height: 50%;
  background-color: red;
  opacity: 0;
  -webkit-transition: opacity 2s ease-in, background-color 2s ease-in;
  -o-transition: opacity 2s ease-in, background-color 2s ease-in;
  transition: opacity 2s ease-in, background-color 2s ease-in;
}

dialog.open {
  background-color: green;
  opacity: 1;
}
<button id="open-button">Open Dialog Element</button>

<dialog>
  <button id="close-button">Close Dialog Element</button>
</dialog>

Upvotes: 3

King Friday
King Friday

Reputation: 26086

Minimal HTML 5 version

The example below has the benefit of no dependencies or external script needed. The <dialog> tag is handy when opened with showModal as it displays a backdrop over the top of DOM declared around it even with display: relative | absolute on its direct parent.

dialog {
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
dialog[open] {
  opacity: 1;
  pointer-events: inherit;
}
dialog::backdrop {
  background-color: rgba(0,0,255, 0.2);
}
<button onclick="dialog.showModal()">show dialog</button>
<dialog id="dialog">
  <p>hi i'm a dialog!</p>
  <form method="dialog">
    <button>Close</button>
  </form>
</dialog>

Using a <form> with method=dialog accomplishes closing the modal without having to handle the close event.

These two references are most enlightening:

https://css-tricks.com/some-hands-on-with-the-html-dialog-element/

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

Closing points

  • As of 11-6-2020, Safari in general does not support this
  • This makes React portals obsolete for modal purposes

Upvotes: 14

Karl Horky
Karl Horky

Reputation: 4934

You can transition the element if you set display: block on it (and allow time for this style to be applied to the element).

Demo: http://jsfiddle.net/v6tbW/11/

To do this with .showModal(), unfortunately it appears that transitions don't work with only the [open] attribute. They do appear to work if you add another class though:

http://jsfiddle.net/karlhorky/eg4n3x18/

Upvotes: 7

Related Questions