# react-zoom-pan-pinch: a TransformWrapper around your own markup

> 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.

**BetterTyped/react-zoom-pan-pinch** — 🖼 React library to support easy zoom, pan, pinch on various html dom elements like <img> and <div>

- Repository: https://github.com/BetterTyped/react-zoom-pan-pinch
- Website: https://bettertyped.github.io/react-zoom-pan-pinch/?path=/docs/basic-image--docs
- Stars: 1,953 · Forks: 303
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/bettertyped-react-zoom-pan-pinch

## 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`.

```jsx
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`.

```jsx
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:

```bash
npm install --save react-zoom-pan-pinch
```

The 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.

## 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: "*"`.

## FAQ

### 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.

## Sources

- [BetterTyped/react-zoom-pan-pinch on GitHub](https://github.com/BetterTyped/react-zoom-pan-pinch)
- [License: MIT](https://github.com/BetterTyped/react-zoom-pan-pinch/blob/master/LICENSE)
- [Project website](https://bettertyped.github.io/react-zoom-pan-pinch/?path=/docs/basic-image--docs)
- [README](https://github.com/BetterTyped/react-zoom-pan-pinch/blob/master/README.md)
- [Releases](https://github.com/BetterTyped/react-zoom-pan-pinch/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bettertyped-react-zoom-pan-pinch
