An earlier article on this site demonstrated how to use the HTML5 <dialog>
element to create easy “lightbox” UI. A few of my web development students are trying to take the pattern further by placing large sections of content in the <dialog>
, but are experiencing a few problems in doing so. I thought I’d use the opportunity to update the script, and show how it can be used in a way to progressively enhance page content.
Rule No. 1: Content First
There are many ways of “hiding” the content for each image, but the first rule of progressive enhancement is not to hide the content at all, at least not by default; the content is already there, on the page, where it can be picked up by search engines and screen readers. In this case, we’re looking at linked thumbnail images that expand to full image versions with descriptive text. Because the text content is extremely lightweight, it will be included in the basic markup of the page. I also used srcset
for the images, although that is not essential to the technique:
<div id="proglight" class="progrock">
<figure>
<img src="sossusvlei-namibia-thumb-1x.jpg" srcset="sossusvlei-namibia-thumb-1x.jpg 1x, sossusvlei-namibia-thumb-2x.jpg 2x" alt="Stunted dead black trees photographed against red dunes and a blue sky">
<figcaption>
<h1>Sossusvlei salt pan, Namib Desert</h1>
<p>One of the harshest and most unforgiving environments on Earth, the Sossusvlei is in the southern part of the Namib Desert, in the African nation of Namibia…
...
There’s also an empty <dialog>
element on the same page:
<dialog id="fullDetails"></dialog>
The other image inside the <div>
takes the same pattern. Next, the CSS sets up the display of each <figure>
element, which will allow us to hide them via JavaScript applied later:
#proglight {
display: flex;
justify-content: space-between;
flex-direction: row;
}
.progrock h1 { font-weight: 100; }
.progrock img {
float: left; width: 50%;
}
.progrock p, .progrock h1 {
margin-left: calc(50% + 2rem);
}
figure.min *:not(img) {
display: none;
}
figure.min img { width: 100%; }
figure.min:hover { cursor: pointer; }
The containing <div>
uses flexbox to push the elements apart; calc
is used to maintain a consistent separation between the images and their associated text. Note the combination of the :not
selector and the .min
class with display: none
, which will ensure that <figure>
elements will only show their images.
The elements don’t have a class by default; that’s applied by JavaScript:
var panels = document.querySelectorAll("#proglight figure"),
dialog = document.getElementById("fullDetails");
Array.prototype.forEach.call(panels, function(panel) {
panel.classList.add("min");
panel.addEventListener("click", function(){
var panelImg = this.getElementsByTagName("img")[0],
panelContent = this.querySelector("figcaption");
showDialog(panelImg, panelContent);
})
});
This portion of the script grabs the <dialog>
element for manipulation, allows the content of each <figure>
panel to be hidden, and adds a call to a function when a panel is clicked, passing it references to the appropriate <img>
and <figcaption>
inside:
function showDialog(panelImg, panelContent) {
var fullImage = document.createElement("img");
fullImage.src = panelImg.src.replace("-thumb", "");
fullImage.alt = panelImg.alt;
var closedialog = document.createElement("button");
closedialog.id = "closeDetails";
closedialog.classList.add("ss-icon");
closedialog.innerHTML = "close";
dialog.appendChild(closedialog);
dialog.appendChild(fullImage);
var textContent = panelContent.cloneNode(true);
dialog.appendChild(textContent);
dialog.classList.add("progrock");
dialog.showModal();
closedialog.onclick = function() {
dialog.classList.add("closer");
setTimeout(function() {
closeDialog() }, (2000))
}
}
The function copies the information from the original <img>
tag, using the filename to generate the information for the new <img>
element in the <dialog>
, together with a progressively enhanced UI element in the form of a close button. The original text content is also cloned and added to the same element, the appearance of which is controlled by more CSS:
dialog {
position: fixed;
left: 50%; top: -50%;
transform: translate(-50%, -50%);
border: none;
background: #fff;
}
dialog[open] {
animation: fallDown 1s .4s forwards;
width: 80%; margin: auto;
max-width: 750px;
padding: 0;
}
dialog[open] img {
width: 100%; height: auto; float: none;
}
dialog[open] p, dialog[open] h1 {
margin-left: 0;
padding: 0 2rem;
}
dialog.closer {
top: 50%;
animation: fallOff 1s .4s forwards;
}
#closeDetails {
position: fixed;
right: -10px;
top: -10px;
border-radius: 50%;
font-size: 1.1rem;
color: #fff;
background: rgba(0,0,0,0.8);
transition: .3s background;
outline: none;
border: 2px solid #ccc;
line-height: 1.3;
padding-top: .3rem;
box-shadow: 0 0 8px rgba(0,0,0,0.3);
}
dialog[open]::backdrop {
animation: fadeToNearBlack 1s forwards;
}
dialog.closer::backdrop {
background: rgba(0,0,0,0.9);
animation: fadeToClear 1s 1s forwards;
}
The position of the “close” button takes advantage of a little-known rule in CSS positioning: a fixed
element inside another element with position: fixed
applied to it is positioned relative to its parent container.
Several animations take over when a <dialog>
element is opened or closed:
@keyframes fadeToNearBlack{
to { background: rgba(0,0,0,0.9); }
}
@keyframes fadeToClear {
to { background: rgba(0,0,0,0); }
}
@keyframes fallDown {
to { top: 50%; }
}
@keyframes fallOff {
to { top: 200%; }
}
Dealing with Mobile
Lightbox effects and dialog windows are commonly associated with, and usually best displayed on, desktop-sized displays; anything more than a small amounts of content will force the dialog window to scroll on small screens, rather defeating the entire point of a lightbox. For this example, we can make hiding the content and generating a <dialog>
conditional on the viewport being at least a certain with by using a matchMedia
rule:
var panels = document.querySelectorAll("#proglight figure"),
minMatch = window.matchMedia("(min-width: 800px)");
if (minMatch.matches) {
Array.prototype.forEach.call(panels, function(panel) {
…
}
This simple test means that the function to create the lightbox effect won’t be run if the browser window opens at 799px wide or less. Note that this simple version isn’t completely responsive, as the condition will not be retested if the user resizes their browser.
We could match this with some CSS to display the complete content at small sizes:
@media screen and (max-width: 800px) {
.progrock img {
float: none;
width: 100%;
display: block;
}
.progrock p, .progrock h1 {
margin-left: 0;
}
}
@media screen and (max-width: 600px) {
#proglight {
flex-direction: column;
}
}
There’s other aspects to the demo, including the use of a semantic ligature icon font, that I’ll leave for investigation in the associated Codepen; it should also be noted that many browsers will require polyfills for some of the advanced features, including support for the <dialog>
element and srcset
. Hopefully this should be enough to get anyone interested started on their own exploration of the possibilities.
Photographs by Asha Wadher, used with permission under a Creative Commons Attribution 3.0 license
Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/LEBjyL