Skip to content

Enable automatic zooming images#4776

Draft
Duncanma wants to merge 4 commits into
mainfrom
zoomable_images
Draft

Enable automatic zooming images#4776
Duncanma wants to merge 4 commits into
mainfrom
zoomable_images

Conversation

@Duncanma

@Duncanma Duncanma commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Zoomable images (Stripe-style click-to-expand)

Summary

Large images in the docs are now click-to-expand, matching the behavior on
Stripe's docs:

  • An image sits in the normal content column at its fitted size.
  • If the image is wider than the space available, hovering it shows a zoom-in cursor and clicking opens a full-width modal of the image at full resolution.
  • Inside the modal the cursor is zoom-out; clicking the image, clicking the backdrop, pressing Escape, or the × button closes it and restores the page.
  • If the image already fits, nothing changes — no cursor change, no modal, no behavioral difference.

The effect is automatic for every image in the docs. Authors do not need to do anything; whether an image is zoomable is decided at runtime by comparing the image's natural pixel width to the width it is actually displayed at.

Opting out: <NoZoom>

Some images are decorative (e.g. the SDK banners on the /develop landing pages) and should never expand, even though they are downscaled. Wrap them in <NoZoom> to disable the click-to-expand affordance:

<NoZoom>

![.NET](/img/assets/banner-dotnet-temporal.png)

</NoZoom>
  • <NoZoom> is registered globally (src/theme/MDXComponents.tsx), so no import is needed in .mdx files. It is also exported from src/components for explicit use.
  • Leave blank lines around the Markdown image inside the wrapper so MDX parses it as Markdown.
  • The wrapper can contain multiple images, links, or captioned images — everything inside it renders as a plain, non-zoomable image.
  • It works by providing a React context flag that ZoomableImage reads (useNoZoom()); when set, the overflow check never marks the image zoomable, so there is no zoom cursor and no modal.

All SDK banners under docs/develop/** are wrapped in <NoZoom>.

What changed

File Change
src/components/images/ZoomableImage.tsx (new) Core component. Detects overflow and renders the modal.
src/components/images/ZoomableImage.module.css (new) Cursor, modal/backdrop, and height: auto styles.
src/theme/MDXComponents.tsx Overrides the global img renderer, so every Markdown image (![alt](src)) flows through ZoomableImage; also registers <NoZoom> globally for opt-out.
src/components/images/CaptionedImage.js Refactored to delegate to ZoomableImage while keeping captions and dark/light support.
src/components/images/ZoomingImage.js, EnlargeImage.js Legacy components; kept working with their existing props but now open the shared modal instead of an inline toggle / new tab.
src/components/images/NoZoom.tsx (new) Context wrapper that opts a subtree of images out of zoom.
src/components/index.js Exports ZoomableImage and NoZoom.

How overflow is detected

On image load and whenever the column is resized (via a ResizeObserver), the component compares img.naturalWidth to the rendered img.clientWidth. The image is treated as zoomable only when it is being downscaled (naturalWidth > clientWidth). This is why the same image can be zoomable on a narrow laptop column (~630px) but not on a wide monitor (~1300px), and why high-resolution ("2x") screenshots — the bulk of our images — are zoomable.

Aspect-ratio fix (important)

Docusaurus adds intrinsic width and height attributes to Markdown images (to prevent layout shift). The stock theme image applies a class that forces height: auto, so the height scales with the width under max-width: 100%. ZoomableImage now applies the same height: auto rule (.image in its CSS module). Without it, images were rendered at their fitted width but full intrinsic height — visibly squished. If you ever see distorted images, this class is the first thing to check.

Benefits

  • Readable detail on dense screenshots. Many UI screenshots are 1600–4096px wide and get downscaled into a ~700px column; users can now open them at full resolution.
  • Zero author overhead. Works on existing and future Markdown images automatically — no new component to remember.
  • No false affordance. Images that already fit are completely untouched, so we don't invite clicks that do nothing.
  • One consistent interaction. The older EnlargeImage (opened a raw image in a new tab), ZoomingImage, and CaptionedImage zoom="true" (inline toggles) now share one modal.
  • Accessible modal. role="dialog", aria-modal, Escape to close, focusable close button, and body-scroll lock while open.

Note: dark/light images now load on demand (performance trade-off)

CaptionedImage supports a srcDark variant. The behavior changed:

  • Before: both the light and dark <img> were rendered into the DOM and swapped with CSS (visibility). Both files were downloaded up front, so toggling the site theme was instant.
  • Now: the active variant is chosen in JavaScript with useColorMode, so only the current theme's image is downloaded.

Trade-off: initial page load fetches one image instead of two (faster, less bandwidth). But if a reader switches the site theme while the page is open, the other variant has not been downloaded yet, so it is fetched on the spot — the swapped image may flash or appear blank for a moment on slow connections or for large files. The fetch is cached after the first switch. This only affects images that actually declare a srcDark; plain Markdown images have a single source and are unaffected.

(in my opinion, this is a fine tradeoff... less bandwidth for the vast majority of cases, I don't believe theme switching is a common activity)

How to test

  1. yarn start and open a page with large images, e.g. /cloud/billing or /develop/go/client/temporal-client.
  2. Hover an oversized image → cursor becomes zoom-in. Click → full-width modal opens. Click the image / backdrop / ×, or press Escape → it closes and the page scroll is restored.
  3. Open a page whose images already fit → confirm there is no zoom cursor and clicking does nothing.
  4. Aspect ratio: confirm images are not squished; compare against production (docs.temporal.io).
  5. Responsive: narrow the browser window and reload — images that fit at full width become zoomable as the column shrinks.
  6. Dark/light + captions: open /codec-server, toggle the navbar theme switch, and confirm the diagrams swap to their dark/light variants and the captions still render. Note the on-demand fetch described above on first toggle.

Pages affected

Pages that previously had explicit zoom — now use the shared modal

These already offered some form of "enlarge". Their interaction is now the unified click-to-expand modal.

Page Previous mechanism
/develop/task-queue-priority-fairness EnlargeImage — click opened the raw image in a new browser tab
/cloud/namespaces ZoomingImage (thumbnail→inline) + CaptionedImage
/cloud/metrics/prometheus-grafana ZoomingImage (thumbnail→inline)
/cloud/high-availability/ha-connectivity CaptionedImage zoom="true" (inline toggle)
/cloud/migrate/migrate-within-cloud CaptionedImage zoom="true" (inline toggle)
/nexus CaptionedImage zoom="true" (inline toggle)
/nexus/execution-debugging CaptionedImage zoom="true" (inline toggle)

Heads-up / follow-ups: the new logic only zooms images that genuinely overflow the column, so a few small images that were manually marked zoomable no longer zoom:

  • migrate-within-cloud.mdx — its four zoom="true" images are only 310–751px wide (they fit the column), so they no longer zoom. They'd need higher-resolution sources to be worth expanding.
  • namespaces.mdx — the ZoomingImage there (cloud-account-id.png, 218px) was a tiny thumbnail; expanding it is no longer meaningful. The large CaptionedImages on the same page do zoom.
  • sdk-metrics-setup.mdx — imports ZoomingImage but never renders it; the unused import can be removed.

Newly zoomable pages (44)

Pages that did not previously have any zoom but contain raster images ≥ 800px wide, which now expand when downscaled. Grouped by area; the width shown is the page's largest image.

The 60 SDK landing pages under /develop/** whose only large image was the shared 1800px banner are excluded: those banners are now wrapped in <NoZoom> (see "Opting out" above), so they no longer zoom.

Best practices

Temporal Cloud

Develop

Develop — dotnet

Develop — go

Develop — java

Develop — python

Develop — ruby

Develop — typescript

Encyclopedia

Evaluate

Production deployment


The page lists above cover raster images (PNG/JPG/etc.), which are the large screenshots that motivated this change. SVG diagrams are vector and stay crisp at any size; they only trigger the zoom affordance if they declare an intrinsic width larger than the column, and expanding them adds little.

@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
temporal-documentation Ready Ready Preview, Comment Jun 26, 2026 7:08pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant