At the next junction, who has right of way?
Answer saved. 39 questions answered before this one.
37/40
Passed
35 correct answers were needed. 3 mistakes to review.
A modal dialog that opens in three beats: the background darkens, two curtains slide in from the edges and meet in the center in 0.6 s, then the content grows in front of them after 0.5 s — and everything closes in reverse order. A real dialog element opened with showModal (inert page behind, focus inside the dialog, Escape), 2.3 KB of JavaScript, final state at once when the user reduces motion.
Updated
This effect is free — the Transitions category contains 52 effects total, including 9 free. Effect.Labs has 840 vanilla effects. Explore the category →
At the next junction, who has right of way?
Answer saved. 39 questions answered before this one.
37/40
35 correct answers were needed. 3 mistakes to review.
Cirque Volpi
Sat, March 14 · 3 pm
Stand B · row 4 · seats 12 to 14
€84Stand B, row 4 · tickets sent by email
COMPAGNIE DU GRENIER
Amateur theater troupe · 2027 season
● The director has published the cast list
First rehearsal Tuesday at 8 pm · community hall
Your role
The dialog is a full-screen <dialog> element (position: fixed, inset: 0, 100% × 100%, no border or background) holding four layers: a .dialog-curtain-backdrop, two curtains .dialog-curtain-left and .dialog-curtain-right, 50% wide each, and the .dialog-curtain-content card centered with translate(-50%, -50%). At rest, the curtains wait off-screen (translateX(-100%) and translateX(100%)), the backdrop is transparent and the card is opacity: 0 and scale(0.8). The .dialog-curtain-open class gives the end state: black backdrop at 50%, curtains at translateX(0), card at 1 and 1.
The three beats come from durations and delays: backdrop in 0.6 s, curtains in 0.6 s on cubic-bezier(0.65, 0, 0.35, 1), card in 0.4 s with transition-delay: 0.5s and a slight bounce. Measured 250 ms after opening: left curtain still at −334 px out of 450, backdrop at 0.35, card invisible; at 700 ms, curtains closed and card at 0.8; everything is in place around 0.9 s (same values in Chromium 151, Firefox 153 and WebKit 26.5). Closing sets .dialog-curtain-closing, which reverses the order: the card fades in 0.2 s, then curtains and backdrop leave with a 0.15 s delay. The script gives this exit 700 ms before calling dialog.close().
The script (2.3 KB) links each .dialog-curtain-btn button to its dialog through aria-controls, which allows several dialogs on the same page. On opening, it calls showModal(), reads offsetWidth and only then adds the class: the dialog leaves display: none, and without that intermediate calculation the browser would have no starting state and would show the end state directly, with none of the three beats. It intercepts the cancel event (Escape) to play the animated closing instead of a dry disappearance, also closes on a click on the backdrop or the curtains — once closed, they cover the whole screen — and resets the classes on every close event, whatever its origin. The .dialog-curtain-preview thumbnail replays the curtains in small in the page; it is optional.
showModal() makes the rest of the page inert (no click or tabbing behind), places the dialog above everything and focuses its first button, "Close" (measured in all three engines). On closing, focus returns to the button that opened it (measured in Chromium and Firefox; in Safari, when it was opened with the keyboard). The dialog is named by its title (aria-labelledby) and the button announces aria-haspopup="dialog".prefers-reduced-motion, every transition of the stage and the dialog is cut: curtains and card take their state directly, and the dialog closes without delay (measured: card at 1 from 250 ms, closed 200 ms after Escape).<dialog> element and showModal(), ::backdrop pseudo-element, CSS transitions + 2.3 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26.5: same three beats on opening, same reverse closing, animated Escape, focus inside the dialog then returned to the button, two independent dialogs on the same page. showModal() exists since Chrome 37, Firefox 98 and Safari 15.4; the inset property since Chrome 87 and Safari 14.1.
Without the dialog element (browsers older than 2022), the script does not wire the button and the dialog stays closed: provide a link to a confirmation page if the content is essential. Without JavaScript, same thing: the button does nothing. Under reduced motion, the dialog opens and closes without animation, with the same focus guarantees.
Copy the three blocks into your page. No dependencies.
<div class="dialog-curtain-stage">
<div class="dialog-curtain-preview" aria-hidden="true">
<div class="dialog-curtain-preview-left"></div>
<div class="dialog-curtain-preview-right"></div>
<span class="dialog-curtain-preview-label">Stage Curtain</span>
</div>
<button class="dialog-curtain-btn" type="button" aria-haspopup="dialog" aria-controls="dialogCurtainModal">Open the dialog</button>
<dialog class="dialog-curtain-modal" id="dialogCurtainModal" aria-labelledby="dialogCurtainTitle">
<div class="dialog-curtain-backdrop"></div>
<div class="dialog-curtain-left"></div>
<div class="dialog-curtain-right"></div>
<div class="dialog-curtain-content">
<h3 id="dialogCurtainTitle">Stage Curtain</h3>
<p>The content appears once the curtains have closed.</p>
<button class="dialog-curtain-close" type="button">Close</button>
</div>
</dialog>
</div>
.dialog-curtain-stage,
.dialog-curtain-stage *,
.dialog-curtain-stage *::before,
.dialog-curtain-stage *::after {
box-sizing: border-box
}
.dialog-curtain-stage {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.dialog-curtain-btn {
padding: 10px 24px;
font-size: 0.85rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
background: #6366f1;
color: white;
transition: background 0.2s, transform 0.1s;
}
.dialog-curtain-btn:hover {
background: #4f46e5;
}
.dialog-curtain-btn:active {
transform: scale(0.96);
}
.dialog-curtain-preview {
position: relative;
width: 200px;
height: 120px;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #0a0a1a;
}
.dialog-curtain-preview-left,
.dialog-curtain-preview-right {
position: absolute;
top: 0;
width: 50%;
height: 100%;
transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.dialog-curtain-preview-left {
left: 0;
background: linear-gradient(135deg, #312e81, #4338ca);
transform: translateX(-100%);
}
.dialog-curtain-preview-right {
right: 0;
background: linear-gradient(225deg, #312e81, #4338ca);
transform: translateX(100%);
}
.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-left {
transform: translateX(0);
}
.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-right {
transform: translateX(0);
}
.dialog-curtain-preview-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
color: white;
font-weight: 600;
font-size: 0.75rem;
z-index: 2;
transition: opacity 0.3s ease 0.4s, transform 0.3s ease 0.4s;
white-space: nowrap;
}
.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-label {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.dialog-curtain-left,
.dialog-curtain-right {
width: 50%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e, #0f172a);
}
.dialog-curtain-left {
transform-origin: left center;
}
.dialog-curtain-right {
transform-origin: right center;
}
.dialog-curtain-modal {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
color: #fff;
overflow: hidden;
font-family: sans-serif;
}
/* Le fond assombri animé est .dialog-curtain-backdrop : celui du navigateur reste transparent. */
.dialog-curtain-modal::backdrop {
background: transparent;
}
.dialog-curtain-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
transition: background 0.6s ease;
}
.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-backdrop {
background: rgba(0, 0, 0, 0.5);
}
.dialog-curtain-modal .dialog-curtain-left,
.dialog-curtain-modal .dialog-curtain-right {
position: absolute;
top: 0;
width: 50%;
height: 100%;
transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.dialog-curtain-modal .dialog-curtain-left {
left: 0;
background: linear-gradient(135deg, #1e1b4b, #312e81);
transform: translateX(-100%);
}
.dialog-curtain-modal .dialog-curtain-right {
right: 0;
background: linear-gradient(225deg, #1e1b4b, #312e81);
transform: translateX(100%);
}
.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-left {
transform: translateX(0);
}
.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-right {
transform: translateX(0);
}
.dialog-curtain-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
background: linear-gradient(135deg, #1e1e2e, #2d2b55);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 32px;
color: white;
text-align: center;
z-index: 2;
transition: opacity 0.4s ease 0.5s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s;
min-width: 200px;
}
.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-content {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-content {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-left {
transform: translateX(-100%);
transition-delay: 0.15s;
}
.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-right {
transform: translateX(100%);
transition-delay: 0.15s;
}
.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-backdrop {
background: rgba(0, 0, 0, 0);
transition-delay: 0.15s;
}
.dialog-curtain-content h3 {
font-size: 1.2rem;
margin: 0 0 8px;
}
.dialog-curtain-content p {
margin: 0 0 16px;
opacity: 0.7;
font-size: 0.85rem;
}
.dialog-curtain-close {
padding: 8px 20px;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
font-size: 0.85rem;
}
.dialog-curtain-close:focus-visible {
outline: 2px solid #c7d2fe;
outline-offset: 3px;
}
/* Mouvement réduit : pas de rideaux qui glissent, chaque élément prend directement son état. */
@media (prefers-reduced-motion: reduce) {
.dialog-curtain-stage *,
.dialog-curtain-modal *,
.dialog-curtain-modal *::before,
.dialog-curtain-modal *::after {
transition: none !important;
}
}
(function() {
/* Dialog Curtain : un vrai <dialog> ouvert en modale (showModal) — le reste de la page devient
inerte, le focus entre dans la fenêtre et revient au bouton à la fermeture. Les classes
ne pilotent que l'animation en trois temps : fond, rideaux, puis contenu. */
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
document.querySelectorAll('.dialog-curtain-btn[aria-controls]').forEach(function(btn) {
var dialog = document.getElementById(btn.getAttribute('aria-controls'));
if (!dialog || typeof dialog.showModal !== 'function') return;
var stage = btn.closest('.dialog-curtain-stage');
var preview = stage && stage.querySelector('.dialog-curtain-preview');
var timer = null;
function reset() {
clearTimeout(timer);
timer = null;
dialog.classList.remove('dialog-curtain-open', 'dialog-curtain-closing');
}
function open() {
if (dialog.open) return;
reset();
dialog.showModal();
void dialog.offsetWidth; /* point de départ des transitions : rideaux ouverts, contenu caché */
dialog.classList.add('dialog-curtain-open');
if (preview) preview.classList.add('dialog-curtain-active');
}
function close() {
if (!dialog.open || timer) return;
dialog.classList.add('dialog-curtain-closing');
if (preview) preview.classList.remove('dialog-curtain-active');
timer = setTimeout(function() {
dialog.close();
}, reduce.matches ? 0 : 700);
}
btn.addEventListener('click', open);
/* Une fois fermés, les rideaux recouvrent tout le fond : un clic dessus ferme aussi. */
dialog.querySelectorAll('.dialog-curtain-close, .dialog-curtain-backdrop, .dialog-curtain-left, .dialog-curtain-right').forEach(function(el) {
el.addEventListener('click', close);
});
/* Échap : la fermeture passe par l'animation inverse au lieu d'être immédiate. */
dialog.addEventListener('cancel', function(e) {
e.preventDefault();
close();
});
/* Quelle que soit la façon dont la fenêtre se ferme, elle repart propre. */
dialog.addEventListener('close', function() {
reset();
if (preview) preview.classList.remove('dialog-curtain-active');
});
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Curtains (CSS — .dialog-curtain-modal .dialog-curtain-left and -right) |
#1e1b4b → #312e81 gradients, 0.6 s on cubic-bezier(0.65, 0, 0.35, 1) |
A repeating-linear-gradient gives velvet folds; 0.9 s makes the closing more theatrical. Change both curtains together. |
Content delay (CSS — transition-delay: 0.5s on .dialog-curtain-content) |
0.5 s: the card arrives as the curtains finish | 0.2 s brings the card in while the curtains are still sliding; beyond 0.6 s, a dead time sets in between closed curtains and content. |
Exit duration (JS — 700 in close()) |
700 ms | It must roughly cover the CSS exit (0.15 s delay + 0.6 s for the curtains): at 700 ms, the curtains are at 99% of their run, nothing visible is lost. If you lengthen the curtains, lengthen this delay too, otherwise the dialog disappears before they are back. |
Darkened backdrop (CSS — rgba(0, 0, 0, 0.5) on .dialog-curtain-open .dialog-curtain-backdrop) |
black at 50% | It only shows while the curtains slide, since they then cover the whole screen. To keep the page visible around, narrow the curtains (40% each): the backdrop then makes full sense. |
Close on click (JS — list .dialog-curtain-close, .dialog-curtain-backdrop, .dialog-curtain-left, .dialog-curtain-right) |
button, backdrop and curtains close | Remove the curtains from the list for a confirmation that must not be closed by mistake; keep at least the button and Escape. |
Preview thumbnail (HTML — .dialog-curtain-preview) |
200 × 120 px, small curtains and a label | Delete it without touching the script: it only shows the pattern in the page. The button is enough to open the dialog. |
Yes. The curtains start from the edges and meet in the center, like a stage curtain coming down; the dialog then presents itself in front of them, and they open again on closing. For the opposite effect, curtains that part to reveal the content, put the card under the curtains (z-index), set the curtains at translateX(0) at rest and at ±100% in .dialog-curtain-open, and remove the card's delay.
A position: fixed div looks like a dialog, but the page behind stays clickable and reachable by keyboard, Escape does nothing and focus stays on the button hidden underneath. showModal() solves all of this at once: inert page, focus inside the dialog then returned to the button, Escape, display above any z-index. The script only adds the animation.
Just before showModal(), the dialog is display: none: it has no displayed state to start from. If the .dialog-curtain-open class arrives in the same frame, the browser computes the end state directly and the transitions do not play (curtains already closed, card already visible). Reading offsetWidth forces a calculation of the "curtains open, card hidden" state; the class added afterwards triggers the three beats. @starting-style would do the same job in recent browsers.