ScreenshotNeo

BlogHow-to

How to Preview an Image on Click with HTML and JavaScript

Build an accessible click-to-preview image with native HTML dialog, or preview a local file with JavaScript object URLs.

By the ScreenshotNeo team1 October 20268 min read

To preview an image when a user clicks it, use a keyboard-operable button that opens a native <dialog> with showModal(). Put the larger image and an explicit close button inside the dialog. For a local file selected with <input type="file">, create an object URL with URL.createObjectURL(file), assign it to the preview image, and revoke the old URL when you replace or remove it.

This guide covers both cases: enlarging an image already on the page and previewing a file before upload. The examples use browser APIs and do not require a library.

1. Existing image: click a thumbnail to open a modal preview

Use a real button instead of making a bare <img> clickable. A button gives keyboard and assistive-technology users an expected control. A modal dialog opened with showModal() makes the rest of the page inert and can be dismissed with Escape. MDN recommends providing a closing mechanism for every dialog and choosing an appropriate initial focus target (MDN dialog reference).

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Image preview</title>
  <style>
    .thumbnail-button {
      border: 0;
      padding: 0;
      background: transparent;
      cursor: zoom-in;
    }

    .thumbnail-button img {
      display: block;
      width: 220px;
      height: 150px;
      object-fit: cover;
    }

    dialog {
      border: 0;
      border-radius: 0.5rem;
      padding: 1rem;
      max-width: min(92vw, 1100px);
      max-height: 92vh;
    }

    dialog::backdrop {
      background: rgb(0 0 0 / 0.78);
    }

    dialog img {
      display: block;
      max-width: 90vw;
      max-height: 80vh;
      width: auto;
      height: auto;
    }

    .dialog-toolbar {
      display: flex;
      justify-content: flex-end;
      margin-bottom: 0.75rem;
    }
  </style>
</head>
<body>
  <button
    id="openPreview"
    class="thumbnail-button"
    type="button"
    aria-label="Preview mountain at full size"
  >
    <img src="mountain-thumb.jpg" alt="Mountain at sunset">
  </button>

  <dialog id="imagePreview" aria-label="Mountain image preview">
    <div class="dialog-toolbar">
      <button id="closePreview" type="button" autofocus>
        Close image preview
      </button>
    </div>
    <img src="mountain-large.jpg" alt="Mountain at sunset, full size">
  </dialog>

  <script>
    const dialog = document.querySelector('#imagePreview');
    const openButton = document.querySelector('#openPreview');
    const closeButton = document.querySelector('#closePreview');

    openButton.addEventListener('click', () => {
      dialog.showModal();
    });

    closeButton.addEventListener('click', () => {
      dialog.close();
      openButton.focus();
    });

    dialog.addEventListener('close', () => {
      // Return focus when Escape or another close method is used.
      openButton.focus();
    });
  </script>
</body>
</html>

The max-width and max-height rules keep a large image inside the viewport. The explicit close button works for touch and pointer users; Escape also closes a modal dialog. The focus return in the close event puts keyboard users back on the thumbnail.

Reuse one dialog and change its image source when a thumbnail is activated. Store the full-size URL in a data-full-src attribute.

<div class="gallery">
  <button class="thumbnail-button" type="button"
          data-full-src="photos/one-large.jpg"
          data-alt="Forest beside a lake">
    <img src="photos/one-thumb.jpg" alt="Forest beside a lake">
  </button>
  <button class="thumbnail-button" type="button"
          data-full-src="photos/two-large.jpg"
          data-alt="Snowy mountain ridge">
    <img src="photos/two-thumb.jpg" alt="Snowy mountain ridge">
  </button>
</div>

<dialog id="galleryPreview" aria-label="Gallery image preview">
  <button id="galleryClose" type="button" autofocus>Close</button>
  <img id="galleryImage" alt="">
</dialog>

<script>
  const galleryDialog = document.querySelector('#galleryPreview');
  const galleryImage = document.querySelector('#galleryImage');
  const galleryClose = document.querySelector('#galleryClose');
  let lastTrigger;

  document.querySelectorAll('.thumbnail-button').forEach((button) => {
    button.addEventListener('click', () => {
      lastTrigger = button;
      galleryImage.src = button.dataset.fullSrc;
      galleryImage.alt = button.dataset.alt || '';
      galleryDialog.showModal();
    });
  });

  galleryClose.addEventListener('click', () => galleryDialog.close());
  galleryDialog.addEventListener('close', () => lastTrigger?.focus());
</script>

2. Preview a local file selected by the user

A local file has no normal URL until the browser exposes it. Read the first selected File, optionally validate its MIME type, and create an object URL. MDN documents this approach for displaying files in web applications (Using files from web applications).

<label for="imageFile">Choose an image</label>
<input id="imageFile" type="file" accept="image/*">

<button id="fileOpen" type="button" disabled>Open preview</button>

<dialog id="fileDialog" aria-label="Selected image preview">
  <button id="fileClose" type="button" autofocus>Close</button>
  <img id="fileImage" alt="Selected image preview">
</dialog>

<script>
  const input = document.querySelector('#imageFile');
  const fileImage = document.querySelector('#fileImage');
  const fileOpen = document.querySelector('#fileOpen');
  const fileDialog = document.querySelector('#fileDialog');
  const fileClose = document.querySelector('#fileClose');
  let objectUrl = null;

  input.addEventListener('change', () => {
    const file = input.files?.[0];

    if (!file) {
      fileOpen.disabled = true;
      fileImage.removeAttribute('src');
      if (objectUrl) URL.revokeObjectURL(objectUrl);
      objectUrl = null;
      return;
    }

    if (!file.type.startsWith('image/')) {
      input.value = '';
      fileOpen.disabled = true;
      alert('Please choose an image file.');
      return;
    }

    if (objectUrl) URL.revokeObjectURL(objectUrl);
    objectUrl = URL.createObjectURL(file);
    fileImage.src = objectUrl;
    fileImage.alt = file.name || 'Selected image';
    fileOpen.disabled = false;
  });

  fileOpen.addEventListener('click', () => fileDialog.showModal());
  fileClose.addEventListener('click', () => fileDialog.close());

  window.addEventListener('beforeunload', () => {
    if (objectUrl) URL.revokeObjectURL(objectUrl);
  });
</script>

Do not revoke the object URL immediately after assigning it if the image must remain visible or interactive. Revoke it when replacing the selection, removing the preview, or leaving the page. A data URL is another documented option: FileReader.readAsDataURL(file) emits a load event whose result can be assigned to img.src (MDN FileReader.readAsDataURL()). The references do not establish a universal performance winner between the two methods.

3. Accessibility checklist

  • Use a <button> or link as the activation control; do not rely on a clickable bare image.
  • Give the thumbnail and full-size image useful, equivalent alternative text.
  • Give the dialog an accessible name with visible text or aria-label.
  • Include a visible close button. Escape is useful but cannot be the only dismissal method.
  • Return focus to the activating control after close.
  • Set initial focus deliberately when the first focusable element is not the right target.
  • Test zoomed layouts and narrow screens; the image should fit without horizontal scrolling.

A custom <div> overlay with role="dialog" and aria-modal="true" does not automatically provide modal focus management, inert background content, Escape handling, or focus restoration. Those behaviors must be implemented yourself (ARIA dialog role, aria-modal). Prefer the native dialog when your browser support target allows it, and check the current compatibility table for the exact features you use.

4. Common errors and fixes

Symptom Cause Fix
showModal is not a function The element is not a dialog, or the target browser lacks the required dialog API. Use <dialog>, load the script after the markup, and check support for your target browsers.
Click does nothing The listener ran before the button existed, or the selector is wrong. Place the script after the HTML or wait for DOM readiness; verify each element is non-null.
Image is huge or cropped No viewport constraints or object-fit: cover applied to the full-size image. Use max-width: 90vw and max-height: 80vh; use object-fit: contain when fitting a fixed box.
Selected file disappears The object URL was revoked while the preview still needed it. Revoke only when replacing or removing the preview.
Dialog cannot reopen Code calls showModal() while the dialog is already open. Guard with if (!dialog.open) dialog.showModal() or disable the trigger while open.
Keyboard focus is lost No focus restoration after close. Store the triggering button and call its focus() in the dialog’s close handler.

5. Performance, reliability, and security notes

  • Use small thumbnails for the page and load the full-size source only when needed. The browser will cache repeat requests according to response headers.
  • Keep one dialog for a gallery instead of creating a dialog for every thumbnail.
  • Validate file type and size in the browser for a quick response, then validate again on the server before storing uploads. A MIME type supplied by the browser is not a security boundary.
  • Object URLs reference local browser memory. Revoke old URLs to avoid retaining unused previews during repeated selections.
  • Do not place untrusted filenames directly into HTML. Assign them with text or an attribute property.
  • For very large images, constrain display dimensions and consider resizing on the server after upload.

6. Or skip the browser setup

If you need screenshots of hosted pages rather than a user-selected local file, ScreenshotNeo returns PNG, JPEG, WebP, or PDF from one GET request. Its cleanup step accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Only clean shots are billed: bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response reports the verdict in X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options. This cURL request saves a WebP screenshot:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));

ScreenshotNeo also provides element and full-page capture, device presets, custom viewports, retina scale, dark mode, waits, custom CSS and JavaScript, request blocking, headers, cookies, user agents, timezone and geolocation, caching with a chosen TTL, signed links, asynchronous jobs, webhooks, bulk capture, usage data, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI agents.

Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

7. FAQ

Can I open the preview without a modal?

Yes. Use dialog.show() for a non-modal dialog, or link directly to the full-size image. A modal is usually clearer when the user should finish viewing before interacting with the page.

How do I close when a user clicks outside the image?

Listen for a click on the dialog and close only when event.target === dialog. Keep the explicit close button as well.

Should I use FileReader or createObjectURL?

Both are documented approaches. Object URLs require lifecycle cleanup with URL.revokeObjectURL(); FileReader produces a data URL after its load event. Choose based on how your upload and preview code is structured.

Can I preview several selected files?

Use multiple, iterate over input.files, create one object URL per file, and revoke every URL when its thumbnail or preview is removed.