react-medium-image-zoom: one wrapper, a native dialog, two image sources
🔎 🏞 The original medium.com-inspired image zooming library for React (since 2016)
At a glance
- What is it?
- A React wrapper that turns a click on an image into a native dialog element, owns the zoom state, and can swap in a sharper file at zoom time. One props interface covers five markup shapes and both state models, and one default in it contradicts the reason the prop exists.
- Who is it for?
- Adopt react-medium-image-zoom if you want one file to own image zoom across React apps, and leave it if you need swipe direction control, locale strings or a mouse drag, because neither props block offers any of those. Before it goes into a product, check that your browser floor includes the dialog element and ResizeObserver, and that your CMS never wraps images in paragraphs unless you pass wrapElement.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A click becomes a native dialog element
Wrapping an image in `<Zoom>` is the entire contract. The component takes your image as `children`, holds the zoomed state itself, and puts a native `<dialog>` element on screen when that state flips, with `classDialog` supplying the class name for the dialog. Two platform APIs sit underneath, and both are named as requirements before any prop is described: `<dialog>` and `ResizeObserver`. Using the browser's own dialog means the top layer, the Escape key and the inert rest of the page come from the platform rather than from hand written overlay code.
That is the argument for adopting it instead of rolling your own. A from scratch version means picking an overlay, moving focus in and out of it, keeping a small picture and a large picture in sync and honouring reduced motion, none of which this component asks you to write. The price is a wrapper element around every image you want to zoom, and a top layer element that has to share the screen with any modal you open while an image is zoomed. If you run your own dialog stack, test that overlap before committing. The project has been shipping since 2016, and it still carries no runtime dependency block in package.json.
npm install and the separate stylesheet import
One command adds the package, and a second import is the part that gets forgotten. The stylesheet is a separate file and nothing injects it for you:
npm install --save react-medium-image-zoomWrap the image, keep the alt text, and import `dist/styles.css` so the transition and the unzoom button have their positioning:
import React from 'react'
import Zoom from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'
export const MyImg = () => (
<Zoom>
<img
alt="That Wanaka Tree, New Zealand by Laura Smetsers"
src="/path/to/thatwanakatree.jpg"
width="500"
/>
</Zoom>
)The exports map in package.json offers exactly two entry points, the package root and `./dist/styles.css`, so that subpath is the supported way to reach the styles and nothing else under dist is addressable. Gatsby, gatsby-plugin-image and Next.js are named as supported tools, so the wrapper drops into a Gatsby or Next.js image component without a provider or a plugin. The sample child carries a width of 500 and a plain src, and the picture example switches files below 800 pixels of viewport, so both wrappers take your own responsive markup as it stands. What you get back from a click is a dialog, not a CSS transform on your own element, which matters if you already have layout code measuring images.
One props interface, two state models
Every option except one lives on `UncontrolledProps`, and the second interface adds a single required field. `ControlledProps` is that same block plus `isZoomed`, which defaults to false:
// Tell the component whether or not it should be zoomed
// Default: false
isZoomed: booleanIn the uncontrolled component the flag is the component's own business. In the controlled one the component renders what you pass and waits for you to change it.
The detail that bites teams moving between the two is `onZoomChange`. Its first argument is the new zoomed state in the uncontrolled case and a suggested new state in the controlled case, and its second is an object holding the event that triggered the change. One callback name, two different contracts. Code written against the default path reads like a notification and behaves like a request, so a parent that ignores the argument still zooms in the uncontrolled build and does nothing at all in the controlled one. Pick the model first, then decide whether the callback is reporting or asking.
`isDisabled` defaults to false and covers the zoom and unzoom behaviour as a whole. It is one boolean for both directions rather than a set of per gesture switches, and the props block documents nothing finer.
zoomImg swaps the file while zoomed
`zoomImg` is typed as `ImgHTMLAttributes<HTMLImageElement>` and applies to the image while it is zoomed, which is the hook for a responsive setup: hand it the high resolution attributes and the small picture in your markup keeps serving the inline view. Nothing else in either interface block changes which file the zoomed state loads, so a second resolution is one prop away.
`ZoomContent` replaces the modal body outright and receives three things: `img`, `buttonUnzoom` and `onUnzoom`, enough to add a caption, a photo credit or a different close control beside the picture. The icon props work the same way, each taking an element type, with `IconZoom` defaulting to `IEnlarge` and `IconUnzoom` to `ICompress`.
`zoomMargin` defaults to 0, so a zoomed image sits flush against the window edges until you set a pixel offset. `a11yNameButtonZoom` and `a11yNameButtonUnzoom` default to 'Expand image' and 'Minimize image'. Those two strings are the only user facing text in the documented interface, and neither block carries a locale or messages property, so an interface in any other language means passing both strings yourself. Both icon defaults are single element types rather than a drawing API, so replacing the arrows means writing an SVG component, not swapping a path constant.
Swipe dismissal and its ten pixel threshold
Touch dismissal is on by default. `canSwipeToUnzoom` starts as true and `swipeToUnzoomThreshold` starts at 10, so a drag past 10 pixels closes the zoom and raising that value is the documented way to make the gesture less eager. Ten pixels is a short distance for a deliberate swipe, which means a stray brush across a zoomed image on a touch laptop is enough to dismiss it.
Neither interface mentions a swipe direction, a mouse drag or a separate pointer threshold, so the gesture surface is touch first. For everyone else the documented dismissal paths are the unzoom button and the dialog itself. If what you zoomed contains text, a chart or a caption, the gap between intent and dismissal reads as a bug in your product rather than a missing feature.
What the props block does not cover is the drag that never reaches the threshold, and it says nothing about suppressing browser gestures while the pointer moves across a zoomed image.
The default wrapElement is div where div is invalid
`wrapElement` accepts 'div' or 'span', and its own comment gives the reason: the image can sit inside a paragraph or a button element, where the wrapper has to be an inline one. The default is 'div'. Read those two lines together and the default is the value that breaks the case the comment exists to solve, since a div is not valid inside a paragraph and a browser is free to close the paragraph before it.
The prop is therefore not optional for the layout it was written for. Any image inside a paragraph, a button or a table cell needs `wrapElement` passed explicitly, and there is no third option for markup that needs something else. That is a one line mistake made once, then a recurring bug class whenever a CMS wraps article images in paragraphs.
Check one more field before trusting a package page: the `homepage` in package.json points at `https://github.com/rpearce/react-medium-image-zoom`, while the worked examples live on the Storybook at `https://rpearce.github.io/react-medium-image-zoom/`.
Zero dependencies against two platform requirements
The feature list opens with zero `dependencies`, and package.json carries no runtime dependency block to contradict it. What the component needs is newer than a bare transpile: `<dialog>` and `ResizeObserver` have to exist in the browser, and the build target is ES2021. For older environments the guidance is to run the package through your own build system, which addresses output syntax and does nothing about supplying either API. No polyfill, feature check or fallback path for the two requirements is documented.
The repository is not archived and the last push was on 2026-09-23. package.json declares version 5.4.9 under BSD-3-Clause, and the tree carries a CHANGELOG.md next to a .changeset/ directory, so upgrade notes land in the repository rather than in an announcement feed.
The examples run through Storybook, served by a single web service defined in compose.yml at the root of the repository:
services:
web:
build: .
command: npm start
ports:
- '6006:6006'
volumes:
- '.:/service'
- '/service/node_modules'Host port 6006 maps to the same port in the container, `npm start` boots the examples, and a separate node_modules volume keeps the host install out of the mounted tree. Clicking through those stories is the fastest way to see what each child markup shape does.
Screen reader coverage is a named list, not a promise
Support is given as a list of pairs rather than a general claim: JAWS and NVDA on Windows in Chrome, Edge and Firefox, VoiceOver in Safari on macOS and iOS, and TalkBack in Chrome on Android. Two absences in that matrix deserve naming. Android outside Chrome and iOS outside Safari are not on the list, and no other platform is mentioned at all, so coverage beyond those pairs is simply undocumented.
The accessibility work shows up in the props as much as in the list. Both zoom controls are buttons, which is why each takes an icon component and a label string of its own, and `wrapElement` exists partly because an image inside a `<button>` would otherwise sit inside another interactive element, with span as the fix. Your markup keeps its own alt text, since children are wrapped rather than rebuilt from a url.
Children can be an img carrying any object-fit and object-position value and `loading="lazy"`, a div or span painted with a background-image, a picture with a source child, a figure holding an img, or an svg. That range is why the prop is typed as ReactNode instead of a source string, and it is why a house image component can be dropped inside as is.
Editorial conclusion
Adopt react-medium-image-zoom if you want one file to own image zoom across React apps, and leave it if you need swipe direction control, locale strings or a mouse drag, because neither props block offers any of those. Before it goes into a product, check that your browser floor includes the dialog element and ResizeObserver, and that your CMS never wraps images in paragraphs unless you pass wrapElement. Zoomed images sit flush against the window edge until you set zoomMargin, and the swipe threshold of 10 pixels dismisses on a short drag, so read both defaults before shipping. Everything else is a prop away.
Frequently asked questions
How do I install react-medium-image-zoom and load its styles?
Run `npm install --save react-medium-image-zoom`, then import `react-medium-image-zoom/dist/styles.css` next to the component import. package.json exports exactly two paths, the package root and that stylesheet, so the styles are a separate import you have to write yourself.
Which browsers and screen readers does react-medium-image-zoom support?
It needs the `<dialog>` element and `ResizeObserver` in the browser, and the build target is `ES2021`. Named screen reader coverage is JAWS and NVDA on Windows in Chrome, Edge and Firefox, VoiceOver in Safari on macOS and iOS, and TalkBack in Chrome on Android.
How do I show a higher resolution image only while zoomed?
Pass the high resolution attributes through `zoomImg`, which is typed as `ImgHTMLAttributes<HTMLImageElement>` and applies to the image while it is zoomed. For full control of the modal body, `ZoomContent` receives the image element, the unzoom button and an unzoom callback.
Does react-medium-image-zoom work with Next.js and Gatsby?
Both are named as supported tools, with gatsby-plugin-image called out by name. The component is a plain React wrapper that takes children, so it needs no provider and no plugin, and it builds on the native dialog element rather than on a library overlay.
How do I stop a swipe from closing a zoomed image?
Set `canSwipeToUnzoom` to false, or raise `swipeToUnzoomThreshold` above its default of 10 pixels. Setting `isDisabled` turns off the zoom and unzoom behaviour as a whole, and it defaults to false.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rpearce-react-medium-image-zoom)Was this useful?