Building an image dialog that works for every visitor means treating the keyboard as a primary input. This article shows a complete pattern using standard HTML, CSS, and JavaScript. The approach uses native dialog elements instead of older lightbox scripts. You can rely on stable browser behavior for focus and Escape handling.
The examples below are fresh demonstration code. They are not part of the historical TopUp project. Historical links and archived documentation pages for TopUp and TopUpPlus establish that project as a JavaScript and jQuery lightbox. Old versions of that lightbox are not claimed to be supported today. The working sample here uses modern dialog primitives to reach the same end goal. It creates a clear, contained view for an image.
Where historical TopUp fits
TopUp began as a JavaScript and jQuery lightbox for image overlays. Its archived documentation and examples show how scripts can open an image container. The script dims the surrounding page. It closes when the user clicks away. Those pages are useful for understanding how early overlays were structured.
When you read older material, notice that keyboard and focus handling were often added by hand. A script could trap Tab. It could listen for Escape. It tried to remember which trigger opened the overlay. Each of those steps required careful code. Browser differences could break the flow.
New code should not copy that structure blindly. Modern browsers support the HTML dialog element with a method that opens a modal layer. It handles much of the background interaction for you. You can focus on layout, content, and return focus. The code in this article follows that path.
Build a dialog that opens on Enter
Start with a button for each image. A button is a natural trigger because it works with mouse, touch, and keyboard. It needs no extra code. When the button receives Enter or Space, it fires a click event. That event can open the dialog.
The dialog itself holds a figure, an image, a caption, and a close button. The close button is the only control inside the dialog. The tab order stays short. The following sample uses a single dialog for simplicity.
<button class="thumb" type="button" data-src="concert-hall.jpg">View image</button>
<dialog id="imageDialog" aria-label="Image preview">
<figure>
<img id="dialogImage" src="" alt="">
<figcaption id="dialogCaption"></figcaption>
</figure>
<button id="closeDialog" type="button">Close</button>
</dialog>
The JavaScript collects the buttons. It stores the element that opened the dialog. It copies the image source and alt text before opening. Storing the last focused element lets you return focus after closing.
const dialog = document.getElementById('imageDialog');
const dialogImage = document.getElementById('dialogImage');
const dialogCaption = document.getElementById('dialogCaption');
const closeButton = document.getElementById('closeDialog');
let lastTrigger = null;
function openImage(trigger) {
lastTrigger = trigger;
dialogImage.src = trigger.dataset.src;
dialogImage.alt = trigger.querySelector('img')
? trigger.querySelector('img').alt : 'Large view';
dialogCaption.textContent = trigger.dataset.caption || '';
dialog.showModal();
}
document.querySelectorAll('.thumb').forEach((trigger) => {
trigger.addEventListener('click', () => openImage(trigger));
});
closeButton.addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => {
if (lastTrigger) lastTrigger.focus();
});
Open the page in a browser. Press Tab until a thumbnail button has focus. Press Enter. The dialog should appear and move focus inside it. Press Escape. The dialog closes. Focus returns to the trigger button. These checks confirm the flow works.
Focus entry and return in practice
When you call the opening method for a modal dialog, the browser places focus inside the dialog. The first focusable element receives focus. In this sample, the close button is that element. A user can leave the dialog with one key press.
Some pages place a large image first. The close control might come later in the markup. The browser will still focus the first focusable item. To control the order, keep interactive elements near the start. Do not rely on tabindex alone. It is easy to break the natural order.
Focus return is your responsibility. The close event on the dialog runs after the layer is hidden. In that handler, call focus on the element that opened the dialog. If that element was removed, fall back to a sensible target. Use the document body or a nearby heading.
Run a quick browser check. Open the dialog. Press Tab a few times. Confirm you cannot tab into the background page. Press Escape. Confirm that focus returns to the trigger. Repeat with the mouse. Use a screen reader if you have one.
Backdrop styling without layers
The backdrop is the dark or tinted layer behind the dialog. Standard CSS supports a backdrop pseudo element. You can style it without extra markup. A semi transparent color helps the image stand out. It keeps the page readable.
dialog {
padding: 0;
border: 0;
background: #fff;
max-width: min(90vw, 720px);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.75);
}
figure {
margin: 0;
}
figcaption {
padding: 8px 12px;
font-size: 0.9rem;
}
button {
margin: 12px;
}
Use viewport units to keep the dialog inside the screen. A maximum width avoids oversized images on wide displays. The backdrop color should provide contrast for the dialog text. It should not make the surrounding page unreadable.
Test with a light and a dark browser theme. Some platforms invert colors automatically. Check that the close button remains visible. Check that the image alt text is clear. Also test at a narrow width. See that the image scales down.
Escape handling and click targets
Escape is a built in way to close a modal dialog. The browser fires the close event. Your handler can restore focus. Do not add a second Escape listener. That can cause duplicate behavior.
Clicking the backdrop is a convenience. Some users want to exit quickly. To detect a click outside, add a listener on the dialog. Compare the event target with the dialog element. If the target is the dialog itself, call the close method.
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
dialog.close();
}
});
Keep the close button visible and reachable. A visible control helps users who do not know Escape works. It also gives screen reader users a clear action to take.
Comparing patterns without claiming support
Historical TopUp code often used a dimming overlay. It used a container that held the image. The basic idea is the same. You hide the rest of the page. You show a focused layer. You allow the user to exit. The difference is in the primitives you use today.
The dialog element provides a modal layer with background inertness. This reduces the risk of focus escaping. It gives you a close event that runs consistently. The event runs when Escape or a close method is used.
Older lightbox scripts often carried their own focus trap. They had key handlers. Those scripts may still work in some contexts. Do not treat them as current. This article clearly separates historical references from the demonstration code above.
Concrete browser checks for your build
Use this short list when you test your page. It focuses on behavior rather than appearance.
- Tab entry: Open with Enter, confirm focus lands inside the dialog.
- Escape close: Press Escape, confirm the dialog hides and focus returns.
- Backdrop click: Click outside the panel, confirm close and focus return.
- Narrow screen: Resize the window and confirm no overflow or clipped controls.
- Screen reader pass: Hear the dialog label, image alt text, and close control in order.
Run these checks in at least one desktop browser. Run them on one mobile browser too. Small differences in focus order can appear. This happens when a virtual keyboard opens. Testing early saves time later.
Practical layout details that keep users oriented
Give the dialog a clear heading or an aria label. Place the close button where it is easy to find. Keep captions short. Long captions can push the close control off screen.
When you show multiple images, reuse one dialog. Swap the image source as needed. This keeps focus handling in one place. Store the trigger each time you open. Return focus will then match the current image.
If you use a grid of thumbnails, make sure each trigger has a descriptive accessible name. A simple approach is to include the alt text of the thumbnail. Put it in the button label. That way, the user knows which image will open.
If the dialog becomes long, add a scrollable region for the content, and keep the close control visible.
Linking to recordings and credits
When an image links to a recording, include a visible link. Do the same for a credit page. Place the link inside the dialog caption. The link should open in the same tab. You only need a new tab if you have a reason. If you open a new tab, add a note in the accessible name. Do not surprise the user.
Keep the dialog focused on the image. Keep it focused on the next action. Users who follow a link should be able to return to the page. They should find the dialog trigger again. Use the focus return handler to support that path.
For historical projects, link to the archived documentation or examples pages. Only link to pages that are still online. Describe them as historical references. Do not imply that those pages ship current code. Do not imply that the project is actively maintained.
A practical next step is to open the developer references for the dialog element. Then adapt the sample above to your own images. Test the keyboard flow with real content. Adjust the backdrop color. Confirm focus returns after every close path. Share a working example in your own repository. Let others review the behavior.
Check the working example
Open the image demo. Use Tab and Shift+Tab to move through its controls. Press Escape to close it. Check that focus returns to the Open image demo button.
const opener = document.getElementById("open-gallery");
const dialog = document.getElementById("image-dialog");
opener.addEventListener("click", () => dialog.showModal());
dialog.addEventListener("close", () => opener.focus());
console.assert(dialog instanceof HTMLDialogElement);
JavaScript image dialogs: keyboard and focus handling
Building an image dialog that works for every visitor means treating the keyboard as a primary input. This article shows a complete pattern using standard HTML, CSS, and JavaScript. The approach uses native dialog elements instead of older lightbox scripts. You can rely on stable browser behavior for focus and Escape handling.
The examples below are fresh demonstration code. They are not part of the historical TopUp project. Historical links and archived documentation pages for TopUp and TopUpPlus establish that project as a JavaScript and jQuery lightbox. Old versions of that lightbox are not claimed to be supported today. The working sample here uses modern dialog primitives to reach the same end goal. It creates a clear, contained view for an image.
Where historical TopUp fits
TopUp began as a JavaScript and jQuery lightbox for image overlays. Its archived documentation and examples show how scripts can open an image container. The script dims the surrounding page. It closes when the user clicks away. Those pages are useful for understanding how early overlays were structured.
When you read older material, notice that keyboard and focus handling were often added by hand. A script could trap Tab. It could listen for Escape. It tried to remember which trigger opened the overlay. Each of those steps required careful code. Browser differences could break the flow.
New code should not copy that structure blindly. Modern browsers support the HTML dialog element with a method that opens a modal layer. It handles much of the background interaction for you. You can focus on layout, content, and return focus. The code in this article follows that path.
Build a dialog that opens on Enter
Start with a button for each image. A button is a natural trigger because it works with mouse, touch, and keyboard. It needs no extra code. When the button receives Enter or Space, it fires a click event. That event can open the dialog.
The dialog itself holds a figure, an image, a caption, and a close button. The close button is the only control inside the dialog. The tab order stays short. The following sample uses a single dialog for simplicity.
The JavaScript collects the buttons. It stores the element that opened the dialog. It copies the image source and alt text before opening. Storing the last focused element lets you return focus after closing.
Open the page in a browser. Press Tab until a thumbnail button has focus. Press Enter. The dialog should appear and move focus inside it. Press Escape. The dialog closes. Focus returns to the trigger button. These checks confirm the flow works.
Focus entry and return in practice
When you call the opening method for a modal dialog, the browser places focus inside the dialog. The first focusable element receives focus. In this sample, the close button is that element. A user can leave the dialog with one key press.
Some pages place a large image first. The close control might come later in the markup. The browser will still focus the first focusable item. To control the order, keep interactive elements near the start. Do not rely on tabindex alone. It is easy to break the natural order.
Focus return is your responsibility. The close event on the dialog runs after the layer is hidden. In that handler, call focus on the element that opened the dialog. If that element was removed, fall back to a sensible target. Use the document body or a nearby heading.
Run a quick browser check. Open the dialog. Press Tab a few times. Confirm you cannot tab into the background page. Press Escape. Confirm that focus returns to the trigger. Repeat with the mouse. Use a screen reader if you have one.
Backdrop styling without layers
The backdrop is the dark or tinted layer behind the dialog. Standard CSS supports a backdrop pseudo element. You can style it without extra markup. A semi transparent color helps the image stand out. It keeps the page readable.
Use viewport units to keep the dialog inside the screen. A maximum width avoids oversized images on wide displays. The backdrop color should provide contrast for the dialog text. It should not make the surrounding page unreadable.
Test with a light and a dark browser theme. Some platforms invert colors automatically. Check that the close button remains visible. Check that the image alt text is clear. Also test at a narrow width. See that the image scales down.
Escape handling and click targets
Escape is a built in way to close a modal dialog. The browser fires the close event. Your handler can restore focus. Do not add a second Escape listener. That can cause duplicate behavior.
Clicking the backdrop is a convenience. Some users want to exit quickly. To detect a click outside, add a listener on the dialog. Compare the event target with the dialog element. If the target is the dialog itself, call the close method.
Keep the close button visible and reachable. A visible control helps users who do not know Escape works. It also gives screen reader users a clear action to take.
Comparing patterns without claiming support
Historical TopUp code often used a dimming overlay. It used a container that held the image. The basic idea is the same. You hide the rest of the page. You show a focused layer. You allow the user to exit. The difference is in the primitives you use today.
The dialog element provides a modal layer with background inertness. This reduces the risk of focus escaping. It gives you a close event that runs consistently. The event runs when Escape or a close method is used.
Older lightbox scripts often carried their own focus trap. They had key handlers. Those scripts may still work in some contexts. Do not treat them as current. This article clearly separates historical references from the demonstration code above.
Concrete browser checks for your build
Use this short list when you test your page. It focuses on behavior rather than appearance.
Run these checks in at least one desktop browser. Run them on one mobile browser too. Small differences in focus order can appear. This happens when a virtual keyboard opens. Testing early saves time later.
Practical layout details that keep users oriented
Give the dialog a clear heading or an aria label. Place the close button where it is easy to find. Keep captions short. Long captions can push the close control off screen.
When you show multiple images, reuse one dialog. Swap the image source as needed. This keeps focus handling in one place. Store the trigger each time you open. Return focus will then match the current image.
If you use a grid of thumbnails, make sure each trigger has a descriptive accessible name. A simple approach is to include the alt text of the thumbnail. Put it in the button label. That way, the user knows which image will open.
Linking to recordings and credits
When an image links to a recording, include a visible link. Do the same for a credit page. Place the link inside the dialog caption. The link should open in the same tab. You only need a new tab if you have a reason. If you open a new tab, add a note in the accessible name. Do not surprise the user.
Keep the dialog focused on the image. Keep it focused on the next action. Users who follow a link should be able to return to the page. They should find the dialog trigger again. Use the focus return handler to support that path.
For historical projects, link to the archived documentation or examples pages. Only link to pages that are still online. Describe them as historical references. Do not imply that those pages ship current code. Do not imply that the project is actively maintained.
A practical next step is to open the developer references for the dialog element. Then adapt the sample above to your own images. Test the keyboard flow with real content. Adjust the backdrop color. Confirm focus returns after every close path. Share a working example in your own repository. Let others review the behavior.
Check the working example
Open the image demo. Use Tab and Shift+Tab to move through its controls. Press Escape to close it. Check that focus returns to the Open image demo button.