Reputation: 28646
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
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:
$("#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
Reputation: 1777
Since you're using jQuery. This is an easier approch:
$(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
Reputation: 8028
You can consider using
dialog[open] {
animation: myFadeIn 5.0s ease normal;
}
@keyframes myFadeIn{
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<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
Reputation: 17240
Expanding on Alexander Nenashev's answer pointing out severe accessibility issues (e.g focusable close buttons):
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 behaviornone
– no chance to see any transition/animation – unless we add some delayNeither the CSS animate
property nor WAAPI
JS interface can circumvent all these restriction.
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">×</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.
: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:
<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
Reputation: 1978
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
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:
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>
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
Reputation: 26086
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
Upvotes: 14
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