Stunted dead black trees photographed against red dunes and a blue sky

Sossusvlei salt pan, Namib Desert

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. Surrounded by high red dunes, the Sossusvlei literally translates as "dead-end marsh", i.e., a drainage area with no outflows. These geographical conditions have created a surreal landscape, making it a popular destination for both tourists and film units: key scenes of The Cell and The Fall were shot here.

Clear chunks of ice backlit by a setting sun on a black sand beach

Breiðamerkursandur, Iceland

Breiðamerkursandur is a black sand beach on the southwest coast of Iceland, adjoining an outwash plain near the glacial lagoon of Jökulsárlón. Every spring and summer ice calving from the tongue of the glacier falls into the Atlantic and is swept out to sea before being returned to the beach, smoothed into sculptured shapes by the waves. Jökulsárlón has been the setting for four Hollywood movies, including A View to a Kill, Die Another Day, Lara Croft: Tomb Raider and Batman Begins.

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