Image zoom and thumbnail expansion across input modes
Give image inspection explicit Expand, Zoom, Reset and Close controls instead of requiring hover or pinch gestures. Use inline expansion when the surrounding explanation should stay in view, or a dialog for a temporary inspection space with a defined return point.
Start with what needs a closer look
The example uses one original inline SVG diagram with three labelled regions. Both presentations contain that diagram, so changing the presentation does not change the artwork. No image is fetched, and there is no remote asset or third-party illustration.
Define the inspection task before choosing an effect. A diagram explaining the paragraph beside it may fit an inline panel. A temporary detour may fit a dialog. Give each a distinct trigger and decide where closing should return the reader before adding scale controls.
This implementation enlarges the complete diagram inside a scroll region. It has no moving magnifier or pointer-following crop. Scale runs from one to three in half-step increments, with a separate stored value for each presentation. Those bounds belong to this diagram, not to every image-zoom task.
Keep the inline panel beside its context
The inline trigger names its panel through aria-controls and exposes expanded state. Opening reveals Zoom in, Zoom out, Reset and Close, followed by the diagram. The opening code does not move focus into the panel. Closing resets the scale and requests focus on the inline trigger.
The scroll region has a visible boundary, a name and a focus target. Reset sets scale to one and writes zero to both scroll offsets. Keep those operations together: restoring a scale value alone would not necessarily return the viewport to its starting position.
Expansion changes the surrounding layout, so retain a useful trigger and explanation beside the open panel. The image should remain part of the reading task rather than becoming a separate activity because there is room for more controls. Close gives the expanded content a predictable end.
Give the dialog a clear opening and exit
The dialog is named, and its Close button carries the initial-focus instruction. The code calls showModal() rather than adding a second scripted focus trap. MDN describes the modal opening method, appropriate initial focus and closing through controls or enabled Escape behavior.1
The close handler resets this presentation’s scale and requests focus on its own trigger. A visible Close button remains available alongside the native cancel path. Those are separate ways to leave, without requiring the reader to have an Escape key or know that convention.
If showModal is unavailable, the dialog case is disabled. The example does not silently substitute another kind of overlay. A fallback would need its own opening, closing and focus rules rather than inheriting a modal label without the corresponding behavior.
Keep state local to each presentation. An inline scale change does not alter the dialog’s stored scale. Reset all provides a broader starting point by closing and resetting both views; it does not merge their triggers or return targets.
The complete example
Save this original, MIT-licensed HTML as a local file. It includes both presentations, all scale controls, reset paths and the original diagram. The SPDX comment identifies its licence. A template supplies the same original SVG to both presentations.
<!doctype html>
<!-- SPDX-License-Identifier: MIT -->
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Inline and dialog image inspection</title>
<style>
[hidden] { display: none !important; }
:focus-visible { outline: 3px solid #a43b00; outline-offset: 2px; }
.viewport { overflow: auto; max-height: 60vh; border: 2px solid #345; }
.art { width: 100%; min-width: 320px; }
svg { display: block; width: 100%; }
dialog { width: min(900px, 85vw); max-height: 90vh; }
</style>
<template id="diagram">
<svg viewBox="0 0 640 320" role="img" aria-label="Original diagram with regions A, B and C">
<rect width="640" height="320" fill="#e9f2f6" />
<circle cx="110" cy="150" r="66" fill="#17738d" />
<rect x="250" y="82" width="135" height="135" fill="#c76e34" />
<path d="M520 70L598 220H442Z" fill="#3b8054" />
<text x="70" y="280">Region A</text>
<text x="265" y="280">Region B</text>
<text x="478" y="280">Region C</text>
</svg>
</template>
<button id="inline-trigger" aria-expanded="false" aria-controls="inline-panel">Expand inline</button>
<div id="inline-panel" hidden>
<button id="inline-plus">Zoom in</button>
<button id="inline-minus">Zoom out</button>
<button id="inline-reset">Reset inline</button>
<button id="inline-close">Close inline</button>
<div id="inline-viewport" class="viewport" tabindex="0" aria-label="Scrollable inline diagram">
<div id="inline-art" class="art"></div>
</div>
</div>
<button id="dialog-trigger">Open diagram dialog</button>
<dialog id="image-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Inspect the diagram</h2>
<button id="dialog-close" autofocus>Close dialog</button>
<button id="dialog-plus">Zoom in dialog</button>
<button id="dialog-minus">Zoom out dialog</button>
<button id="dialog-reset">Reset dialog</button>
<div id="dialog-viewport" class="viewport" tabindex="0" aria-label="Scrollable dialog diagram">
<div id="dialog-art" class="art"></div>
</div>
</dialog>
<button id="reset-all">Close and reset all</button>
<script>
const inlineTrigger = document.querySelector('#inline-trigger');
const inlinePanel = document.querySelector('#inline-panel');
const dialogTrigger = document.querySelector('#dialog-trigger');
const dialog = document.querySelector('#image-dialog');
const scale = { inline: 1, dialog: 1 };
function resize(kind, value) {
scale[kind] = Math.max(1, Math.min(3, value));
document.querySelector('#' + kind + '-art').style.width = scale[kind] * 100 + '%';
if (scale[kind] === 1) {
const viewport = document.querySelector('#' + kind + '-viewport');
viewport.scrollTop = 0;
viewport.scrollLeft = 0;
}
}
function closeInline(returnFocus = true) {
inlinePanel.hidden = true;
inlineTrigger.setAttribute('aria-expanded', 'false');
resize('inline', 1);
if (returnFocus) inlineTrigger.focus();
}
inlineTrigger.addEventListener('click', () => {
if (!inlinePanel.hidden) return closeInline();
inlinePanel.hidden = false;
inlineTrigger.setAttribute('aria-expanded', 'true');
});
document.querySelector('#inline-close').addEventListener('click', () => closeInline());
inlinePanel.addEventListener('keydown', event => {
if (event.key === 'Escape') closeInline();
});
for (const kind of ['inline', 'dialog']) {
const picture = document.querySelector('#diagram').content.cloneNode(true);
document.querySelector('#' + kind + '-art').append(picture);
document.querySelector('#' + kind + '-plus').addEventListener('click', () => resize(kind, scale[kind] + .5));
document.querySelector('#' + kind + '-minus').addEventListener('click', () => resize(kind, scale[kind] - .5));
document.querySelector('#' + kind + '-reset').addEventListener('click', () => resize(kind, 1));
}
dialogTrigger.disabled = typeof dialog.showModal !== 'function';
dialogTrigger.addEventListener('click', () => {
resize('dialog', 1);
dialog.showModal();
});
document.querySelector('#dialog-close').addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => {
resize('dialog', 1);
dialogTrigger.focus();
});
document.querySelector('#reset-all').addEventListener('click', () => {
closeInline(false);
if (dialog.open) dialog.close();
resize('dialog', 1);
});
</script>
</html>
Walk through opening, scaling and returning
The demo retains the instrumented version. Use the trimmed component above to follow each presentation through its own state:
- Open inline. The trigger changes expanded state and reveals the panel without asking another element to take focus. The diagram starts from that presentation’s stored scale.
- Zoom in or out. The handler adjusts scale by a half step within the one-to-three bounds, then updates the diagram’s width. It does not change browser zoom or the other presentation’s scale.
- Reset while enlarged. Reset restores scale one and requests zero horizontal and vertical scroll positions. Let an ongoing keyboard scroll settle before using that starting point. An open panel at scale one remains different from a closed panel.
- Close inline with Escape. The inline closing path resets scale, hides the panel and requests the corresponding trigger. The return target belongs to this presentation, rather than being whichever button happens to come first on the page.
- Open and close the dialog. The trigger requests a modal view. Explicit Close and the native cancel route lead to the dialog’s closing path, which resets scale and requests the dialog trigger. Reopening therefore does not silently preserve an earlier enlargement.
- Reset everything. Reset all closes both presentations and restores their starting state. Use that broader reset when beginning another comparison, rather than carrying one presentation’s enlargement into the next task.
The diagram and scale rules are shared, while the opening and return points remain separate. That keeps the comparison about presentation and lifecycle instead of changing the image at the same time. It also gives application code a clear place to connect its own return target.
Keep image scale separate from page zoom
Every scale change in this example has an explicit button. The code does not intercept pinch gestures or replace browser zoom. Its scale number describes the diagram’s rendered width, not browser magnification or effective image detail.
An enlarged vector diagram says nothing about the resolution of a photograph. A raster source can only supply the detail it contains. Before adapting the controls, decide whether the actual image has useful detail at the intended size and whether the site has permission to use it.
Keep the explanation available alongside inspection or return to it when closing. Larger controls and a larger image are useful when they help answer the reader’s question. The task should remain understandable without a decorative animation or a gesture tutorial.
Replacing an existing integration
Map the application’s expansion, scale and closing requirements, then connect original code to its actual content and return targets. This route reuses no former zoom plugin, image or article and promises no compatibility with an earlier API.
Not covered on this page
- Moving magnifiers, pointer-following crops or image editing.
- Raster source-resolution or photographic-detail assessments.
- A universal choice between inline and dialog presentation.
- Accessibility certification or compatibility with former plugins.
Sources
-
MDN, The dialog element. Living documentation, opened 24 October 2026. ↩
Code on this page is original and MIT licensed.
How we test: methodology. Found a mistake? Corrections.