react-zoom-pan-pinch: a TransformWrapper around your own markup
🖼 React library to support easy zoom, pan, pinch on various html dom elements like <img> and <div>
At a glance
- What is it?
- react-zoom-pan-pinch gives a React component pair that turns any DOM subtree into a zoomable, pannable surface with touch, wheel and mouse handling, and it ships no runtime dependencies. The interesting part is that the transform state lives in context, so controls can live elsewhere under TransformWrapper, outside the pannable area.
- Who is it for?
- Adopt this package when you need a pan and zoom surface whose contents are ordinary React children, since wrapping markup costs one component pair and no extra dependencies. Skip it when you need server-rendered zoom state or controlled transform values from outside React, because the README documents only context and hook access rather than a controlled component API.
- Can I use it commercially?
- Yes. MIT 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 last received commits 8 days ago.
- 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
Two components and a wrapper, not a canvas
The library attaches gesture handling to real DOM elements you already have. The smallest example in the README puts an ordinary `img` inside `TransformComponent`, which itself sits inside `TransformWrapper`.
import React, { Component } from "react";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
const Example = () => {
return (
<TransformWrapper>
<TransformComponent>
<img src="image.jpg" alt="test" />
</TransformComponent>
</TransformWrapper>
);
};That structure is the whole mental model. `TransformWrapper` owns the state and the event surface, `TransformComponent` is the element whose children move, and anything you render between them, an image, a div, SVG, a chart, is content rather than configuration. Nothing is drawn to a canvas, so your existing CSS and semantics survive.
The package is described in the README as light and without external dependencies, and `package.json` confirms it: `react` and `react-dom` appear only as peer dependencies, declared as `"*"`. Bundlers therefore get no transitive runtime code to reason about.
Context is why controls can sit outside the pannable area
The second README example is more instructive than the first, because it separates where the content lives from where the controls live. A `Controls` component calls `useControls()` and renders three buttons, and that component is placed inside `TransformWrapper` but as a sibling of `TransformComponent`.
import {
TransformWrapper,
TransformComponent,
useControls,
} from "react-zoom-pan-pinch";
const Controls = () => {
const { zoomIn, zoomOut, resetTransform } = useControls();
return (
<div className="tools">
<button type="button" onClick={() => zoomIn()}>
+
</button>
<button type="button" onClick={() => zoomOut()}>
-
</button>
<button type="button" onClick={() => resetTransform()}>
x
</button>
</div>
);
};Any component rendered under the wrapper can call `useControls()` and get `zoomIn`, `zoomOut` and `resetTransform` back. That is the mechanism behind the README's claim of context based usage, and it is what lets a toolbar stay fixed while the content pans beneath it.
Initial state is set on the wrapper rather than through the hook, using props named for exactly what they set: `initialScale`, `initialPositionX` and `initialPositionY`. The example uses `initialScale={1}` with `initialPositionX={200}` and `initialPositionY={100}`.
Installing from npm with either package manager
Installation is a single package addition, and the README gives both forms:
npm install --save react-zoom-pan-pinchThe Yarn equivalent is `yarn add react-zoom-pan-pinch`. There is no provider to mount, no global CSS file to import and no configuration step. After installation, the three names you need are exported from the package root: `TransformWrapper`, `TransformComponent` and `useControls`.
For a first real use, wrap whatever you currently render in a fixed size container, put your existing content inside `TransformComponent`, and check that dragging with a mouse moves it. If nothing moves, the usual first check is that the wrapper element has a bounded height, since a component with no height has nothing to pan within. The library handles mobile touch gestures, touchpad gestures and desktop mouse events, so the same markup covers phones and desktops.
Where the build pipeline sits, and why it matters to you
The package ships three entry formats, which tells you how it will land in your app. `main` points at `dist/index.cjs.js` for CommonJS, `module` at `dist/index.esm.js` for ES modules, and `types` at `dist/index.d.ts`, with `source` set to `src/index.ts`. Notably, `files` includes both `dist` and `src`, so the TypeScript sources travel in the tarball too, which is useful when you need to trace a behaviour back to source.
The build itself is Rollup driven, since `build` runs `rollup -c` against `rollup.config.mjs`. Types are produced by TypeScript and verified separately through `tsc --noEmit --emitDeclarationOnly false`. Tests run under Jest with a `jest.config.ts` and `jest.setup.ts`, and there is an `__tests__/` directory at the root.
Documentation is Storybook. The `storybook` script serves on port 6006, the docs build prefixes paths with `STORYBOOK_BASE=/react-zoom-pan-pinch/`, and deploys go out through `gh-pages`. That is also why the README points at a hosted docs URL with a path of `/docs/basic-image--docs` and a props reference at `/docs/docs-props--docs`.
Limits: transform state you cannot drive from the outside
The API surface described in the README is initial props in, imperative actions out through `useControls()`. There is no documented controlled mode where you pass `scale` and `position` as props and the component obeys you, and there is no documented callback reporting the current scale when a gesture ends. If your application needs a zoom level in a URL, or needs a slider bound to the zoom, you are outside what the documented API covers and would have to reach for the underlying utilities.
The README also advertises animations and utilities for building your own tools, plus advanced hooks and components, without documenting them in the text itself. The docs site carries the prop reference, so treat the repository README as an introduction rather than a complete specification.
Version compatibility is another practical limit. The peer range is `"*"`, which means nothing is enforced at install time. If you are on a React version whose event model differs, you will find out at runtime rather than during install.
Against a gesture library, and against the native CSS transform
There are two reasonable alternatives and they solve different problems.
The first is a general purpose gesture or transform library that treats pan and zoom as one case of a larger interaction system, with recognisers, velocity and multi touch arbitration. This package does the opposite: it goes deep on one interaction and exposes nothing else, which is why the README can claim no external dependencies and why its bundle badge is worth looking at if size matters to you.
The second alternative is writing the transform yourself with CSS `transform` and a handful of pointer handlers, or using native browser zoom and pan on an `iframe`. For a static image viewer with no controls, that is often less code than a dependency. The case for the package is the parts it handles that are fiddly by hand: keeping a drag and a pinch consistent, providing `resetTransform`, and letting control buttons live outside the panned element through context.
What this package does not compete with is a full canvas or WebGL scene graph. It moves your DOM.
Editorial conclusion
Adopt this package when you need a pan and zoom surface whose contents are ordinary React children, since wrapping markup costs one component pair and no extra dependencies. Skip it when you need server-rendered zoom state or controlled transform values from outside React, because the README documents only context and hook access rather than a controlled component API. Verify first that your React version satisfies the peer range, which is declared as `react: "*"` and `react-dom: "*"`.
Frequently asked questions
How do I install react-zoom-pan-pinch?
Run `npm install --save react-zoom-pan-pinch`, or `yarn add react-zoom-pan-pinch` with Yarn. Then import `TransformWrapper` and `TransformComponent` from the package root and wrap your markup, with no provider or CSS import required.
How do I add zoom in, zoom out and reset buttons outside the panned area?
Render a component under `TransformWrapper` that calls `useControls()`, which returns `zoomIn`, `zoomOut` and `resetTransform`. The README example places that component as a sibling of `TransformComponent` so the buttons stay outside the moving content.
How do I set the starting zoom and position in react-zoom-pan-pinch?
Pass `initialScale`, `initialPositionX` and `initialPositionY` to `TransformWrapper`. The README example uses `initialScale={1}` with `initialPositionX={200}` and `initialPositionY={100}`.
Does react-zoom-pan-pinch work with touch and mouse on the same component?
The README lists mobile gestures, touchpad gestures and desktop mouse events as supported, so the same markup covers touch devices and desktops. The examples wrap plain DOM elements such as `img` and `div`.
What are the peer dependencies of react-zoom-pan-pinch?
Only `react` and `react-dom`, both declared with the range `"*"` in `package.json`. The library itself has no external runtime dependencies.
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/bettertyped-react-zoom-pan-pinch)Was this useful?