{"slug":"image-cropper","title":"Image Cropper","description":"Using the Image Cropper machine in your project.","contentType":"component","framework":"react","content":"Image cropper lets users select and edit an image crop area with pan, zoom,\nrotation, and flip controls.\n\n## Resources\n\n\n[Latest version: v1.35.3](https://www.npmjs.com/package/@zag-js/image-cropper)\n[Logic Visualizer](https://zag-visualizer.vercel.app/image-cropper)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/image-cropper)\n\n\n\n**Features**\n\n- Pan, zoom, rotate, and flip with pointer, wheel, and pinch gestures\n- Rectangle or circle crops with optional fixed area and aspect ratio\n- Min/max crop dimensions and keyboard nudging\n- Controlled zoom/rotation/flip with change callbacks\n- Programmatic helpers like `api.resize` and `api.getCroppedImage`\n- Accessibility labels, translations, and styling data attributes\n\n## Installation\n\nInstall the image cropper package:\n\n```bash\nnpm install @zag-js/image-cropper @zag-js/react\n# or\nyarn add @zag-js/image-cropper @zag-js/react\n```\n\n## Anatomy\n\nTo set up the image cropper correctly, you'll need to understand its anatomy and\nhow we name its parts.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the image cropper package:\n\n```jsx\nimport * as imageCropper from \"@zag-js/image-cropper\"\n```\n\nThe package exports two key functions:\n\n- `machine` - State machine logic.\n- `connect` - Maps machine state to JSX props and event handlers.\n\nThen use the framework integration helpers:\n\n```jsx\nimport * as imageCropper from \"@zag-js/image-cropper\"\nimport { normalizeProps, useMachine } from \"@zag-js/react\"\nimport { useId } from \"react\"\n\nexport function ImageCropper() {\n  const service = useMachine(imageCropper.machine, {\n    id: useId(),\n  })\n\n  const api = imageCropper.connect(service, normalizeProps)\n\n  return (\n    <div {...api.getRootProps()}>\n      <div {...api.getViewportProps()}>\n        <img\n          src=\"https://picsum.photos/seed/crop/640/400\"\n          crossOrigin=\"anonymous\"\n          {...api.getImageProps()}\n        />\n\n        <div {...api.getSelectionProps()}>\n          {imageCropper.handles.map((position) => (\n            <div key={position} {...api.getHandleProps({ position })}>\n              <span />\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n```\n\n### Setting the initial crop\n\nPass an `initialCrop` to start from a specific rectangle. The size is\nconstrained to your min/max and viewport, and the position is clamped within the\nviewport.\n\n```jsx {2-6}\nconst service = useMachine(imageCropper.machine, {\n  initialCrop: { x: 40, y: 40, width: 240, height: 240 },\n  aspectRatio: 1, // optional, lock to square\n})\nconst api = imageCropper.connect(service, normalizeProps)\n```\n\n### Fixed crop area\n\nLock the crop window and allow only panning/zooming of the image beneath it by\nsetting `fixedCropArea: true`.\n\n```jsx {2}\nconst service = useMachine(imageCropper.machine, {\n  fixedCropArea: true,\n})\n```\n\n### Crop shape and aspect ratio\n\n- `cropShape` can be `\"rectangle\"` or `\"circle\"`.\n- `aspectRatio` can lock the crop to a width/height ratio. When `aspectRatio` is\n  not set and `cropShape` is `\"rectangle\"`, holding Shift while resizing locks\n  to the current ratio.\n\n```jsx {2-3}\nconst service = useMachine(imageCropper.machine, {\n  cropShape: \"circle\",\n  aspectRatio: 1, // ignored for circle\n})\n```\n\n### Controlling zoom, rotation, and flip\n\nYou can configure defaults and limits, and also control them programmatically\nusing the API.\n\n```jsx {2-6}\nconst service = useMachine(imageCropper.machine, {\n  defaultZoom: 1.25,\n  minZoom: 1,\n  maxZoom: 5,\n  defaultRotation: 0,\n  defaultFlip: { horizontal: false, vertical: false },\n})\nconst api = imageCropper.connect(service, normalizeProps)\n\n// Programmatic controls\napi.setZoom(2) // zoom to 2x\napi.setRotation(90) // rotate to 90 degrees\napi.flipHorizontally() // toggle horizontal flip\napi.setFlip({ vertical: true }) // set vertical flip on\n```\n\n### Controlled transform values\n\nUse controlled props and callbacks when transform values are managed by your\nstate.\n\n```jsx\nconst service = useMachine(imageCropper.machine, {\n  zoom,\n  rotation,\n  flip,\n  onZoomChange(details) {\n    setZoom(details.zoom)\n  },\n  onRotationChange(details) {\n    setRotation(details.rotation)\n  },\n  onFlipChange(details) {\n    setFlip(details.flip)\n  },\n})\n```\n\n### Listening for crop changes\n\nUse `onCropChange` to react when the crop rectangle changes.\n\n```jsx\nconst service = useMachine(imageCropper.machine, {\n  onCropChange(details) {\n    // details => { crop: Rect }\n    console.log(details.crop)\n  },\n})\n```\n\n### Programmatic resizing\n\nUse `api.resize(handle, delta)` to resize from any handle programmatically.\nPositive `delta` grows outward, negative shrinks inward.\n\n```jsx\n// Grow the selection by 8px from the right edge\napi.resize(\"right\", 8)\n// Shrink from top-left corner by 4px in both axes\napi.resize(\"top-left\", -4)\n```\n\n### Getting the cropped image\n\nUse `api.getCroppedImage` to export the current crop, taking\nzoom/rotation/flip/pan into account.\n\n```jsx\n// Blob (default)\nconst blob = await api.getCroppedImage({ type: \"image/png\", quality: 0.92 })\n\n// Data URL\nconst dataUrl = await api.getCroppedImage({\n  output: \"dataUrl\",\n  type: \"image/jpeg\",\n  quality: 0.85,\n})\n\n// Example usage\nif (blob) {\n  const url = URL.createObjectURL(blob)\n  previewImg.src = url\n}\n```\n\n### Understanding coordinate systems\n\nThe image cropper uses two different coordinate systems:\n\n#### 1. Viewport Coordinates (`api.crop`)\n\nThese are the coordinates you see in the UI, relative to the visible viewport:\n\n```jsx\nconsole.log(api.crop)\n// { x: 50, y: 30, width: 200, height: 150 }\n```\n\n**Characteristics:**\n\n- Relative to the viewport dimensions\n- Changes as you zoom and pan\n- Perfect for UI rendering and controls\n- Used by `initialCrop`\n\n#### 2. Natural Image Coordinates (`api.getCropData()`)\n\nThese are the absolute pixel coordinates in the original image:\n\n```jsx\nconst cropData = api.getCropData()\nconsole.log(cropData)\n// {\n//   x: 250,\n//   y: 150,\n//   width: 1000,\n//   height: 750,\n//   rotate: 0,\n//   flipX: false,\n//   flipY: false\n// }\n```\n\n**Characteristics:**\n\n- Relative to the original image dimensions\n- Independent of zoom/pan/viewport size\n- Essential for server-side cropping\n- Perfect for state persistence and undo/redo\n\n#### When to use each\n\n**Use viewport coordinates (`api.crop`)** when:\n\n- Rendering UI controls (sliders, displays)\n- Setting initial crop area\n- Building custom crop UI\n\n**Use natural coordinates (`api.getCropData()`)** when:\n\n- Sending crop data to your backend for server-side processing\n- Persisting state (localStorage, database)\n- Implementing undo/redo functionality\n- Exporting crop configuration to external tools\n\n#### Example: Server-side cropping\n\n```jsx\n// Frontend: Get natural coordinates\nconst cropData = api.getCropData()\n\n// Send to backend\nawait fetch(\"/api/crop-image\", {\n  method: \"POST\",\n  body: JSON.stringify({\n    imageId: \"photo-123\",\n    crop: cropData, // Natural pixel coordinates\n  }),\n})\n\n// Backend: Crop the original image file\n// Use cropData.x, cropData.y, cropData.width, cropData.height\n// to crop the actual image file at full resolution\n```\n\n#### Transformation example\n\nHere's how the coordinates relate with a zoom of 2x:\n\n```jsx\n// Original image: 3000 × 2000 pixels\n// Viewport: 600 × 400 pixels\n// Zoom: 2x\n\n// Viewport coordinates (what you see)\napi.crop\n// { x: 100, y: 80, width: 200, height: 150 }\n\n// Natural coordinates (original image)\napi.getCropData()\n// { x: 500, y: 400, width: 1000, height: 750, ... }\n// Scale factor: 3000 / 600 = 5x\n// So 100px in viewport = 500px in original image\n```\n\n### Touch and wheel gestures\n\n- Use the mouse wheel over the viewport to zoom at the pointer location.\n- Pinch with two fingers to zoom and pan; the machine smooths tiny changes and\n  tracks the pinch midpoint.\n- Drag on the viewport background to pan the image (when not dragging the\n  selection).\n\n### Keyboard nudges\n\nConfigure keyboard nudge steps for move/resize:\n\n```jsx {2-4}\nconst service = useMachine(imageCropper.machine, {\n  nudgeStep: 1,\n  nudgeStepShift: 10,\n  nudgeStepCtrl: 50,\n})\n```\n\n### Accessibility\n\n- The root is a live region with helpful descriptions of crop, zoom, and\n  rotation status.\n- The selection exposes slider-like semantics to assistive tech and supports\n  keyboard movement, resizing (Alt+Arrows), and zooming (+/-).\n- Customize accessible labels and descriptions via `translations`:\n\n```jsx {2-7}\nconst service = useMachine(imageCropper.machine, {\n  translations: {\n    rootLabel: \"Product image cropper\",\n    selectionInstructions:\n      \"Use arrow keys to move, Alt+arrows to resize, and +/- to zoom.\",\n  },\n})\n```\n\n## Styling guide\n\nEach part includes a `data-part` attribute you can target in CSS.\n\n```css\n[data-scope=\"image-cropper\"][data-part=\"root\"] {\n  /* styles for the root part */\n}\n\n[data-scope=\"image-cropper\"][data-part=\"viewport\"] {\n  /* styles for the viewport part */\n}\n\n[data-scope=\"image-cropper\"][data-part=\"image\"] {\n  /* styles for the image part */\n}\n\n[data-scope=\"image-cropper\"][data-part=\"selection\"] {\n  /* styles for the selection part */\n}\n\n[data-scope=\"image-cropper\"][data-part=\"handle\"] {\n  /* styles for the handle part */\n}\n```\n\n### Selection shapes\n\nThe selection can be styled based on its shape:\n\n```css\n[data-part=\"selection\"][data-shape=\"circle\"] {\n  /* styles for circular selection */\n}\n\n[data-part=\"selection\"][data-shape=\"rectangle\"] {\n  /* styles for rectangular selection */\n}\n```\n\n### States\n\nVarious states can be styled using data attributes:\n\n```css\n[data-part=\"root\"][data-dragging] {\n  /* styles when dragging the selection */\n}\n\n[data-part=\"root\"][data-fixed] {\n  /* styles when the crop area is fixed */\n}\n```\n\n## Keyboard Interactions\n\n**`ArrowUp`**\nDescription: Moves the crop selection upward by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.\n\n**`ArrowDown`**\nDescription: Moves the crop selection downward by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.\n\n**`ArrowLeft`**\nDescription: Moves the crop selection to the left by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.\n\n**`ArrowRight`**\nDescription: Moves the crop selection to the right by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.\n\n**`Alt + ArrowUp`**\nDescription: Resizes the crop vertically from the bottom handle, reducing the height. Hold Shift or Ctrl/Cmd for the larger nudge steps.\n\n**`Alt + ArrowDown`**\nDescription: Resizes the crop vertically from the bottom handle, increasing the height. Hold Shift or Ctrl/Cmd for the larger nudge steps.\n\n**`Alt + ArrowLeft`**\nDescription: Resizes the crop horizontally from the right handle, reducing the width. Hold Shift or Ctrl/Cmd for the larger nudge steps.\n\n**`Alt + ArrowRight`**\nDescription: Resizes the crop horizontally from the right handle, increasing the width. Hold Shift or Ctrl/Cmd for the larger nudge steps.\n\n**`+`**\nDescription: Zooms in on the image. The `=` key performs the same action on keyboards where both symbols share a key.\n\n**`-`**\nDescription: Zooms out of the image. The `_` key performs the same action on keyboards where both symbols share a key.\n\n## Methods and Properties\n\n### Machine Context\n\nThe image cropper machine exposes the following context properties:\n\n**`ids`**\nType: `Partial<{ root: string; viewport: string; image: string; selection: string; handle: (position: string) => string; }>`\nDescription: The ids of the image cropper elements\n\n**`translations`**\nType: `IntlTranslations`\nDescription: Specifies the localized strings that identify accessibility elements and their states.\n\n**`initialCrop`**\nType: `Rect`\nDescription: The initial rectangle of the crop area.\nIf not provided, a smart default will be computed based on viewport size and aspect ratio.\n\n**`minWidth`**\nType: `number`\nDescription: The minimum width of the crop area\n\n**`minHeight`**\nType: `number`\nDescription: The minimum height of the crop area\n\n**`maxWidth`**\nType: `number`\nDescription: The maximum width of the crop area\n\n**`maxHeight`**\nType: `number`\nDescription: The maximum height of the crop area\n\n**`aspectRatio`**\nType: `number`\nDescription: The aspect ratio to maintain for the crop area (width / height).\nFor example, an aspect ratio of 16 / 9 will maintain a width to height ratio of 16:9.\nIf not provided, the crop area can be freely resized.\n\n**`cropShape`**\nType: `\"rectangle\" | \"circle\"`\nDescription: The shape of the crop area.\n\n**`zoom`**\nType: `number`\nDescription: The controlled zoom level of the image.\n\n**`rotation`**\nType: `number`\nDescription: The controlled rotation of the image in degrees (0 - 360).\n\n**`flip`**\nType: `FlipState`\nDescription: The controlled flip state of the image.\n\n**`defaultZoom`**\nType: `number`\nDescription: The initial zoom factor to apply to the image.\n\n**`defaultRotation`**\nType: `number`\nDescription: The initial rotation to apply to the image in degrees.\n\n**`defaultFlip`**\nType: `FlipState`\nDescription: The initial flip state to apply to the image.\n\n**`zoomStep`**\nType: `number`\nDescription: The amount of zoom applied per wheel step.\n\n**`zoomSensitivity`**\nType: `number`\nDescription: Controls how responsive pinch-to-zoom is.\n\n**`minZoom`**\nType: `number`\nDescription: The minimum zoom factor allowed.\n\n**`maxZoom`**\nType: `number`\nDescription: The maximum zoom factor allowed.\n\n**`nudgeStep`**\nType: `number`\nDescription: The base nudge step for keyboard arrow keys (in pixels).\n\n**`nudgeStepShift`**\nType: `number`\nDescription: The nudge step when Shift key is held (in pixels).\n\n**`nudgeStepCtrl`**\nType: `number`\nDescription: The nudge step when Ctrl/Cmd key is held (in pixels).\n\n**`onZoomChange`**\nType: `(details: ZoomChangeDetails) => void`\nDescription: Callback fired when the zoom level changes.\n\n**`onRotationChange`**\nType: `(details: RotationChangeDetails) => void`\nDescription: Callback fired when the rotation changes.\n\n**`onFlipChange`**\nType: `(details: FlipChangeDetails) => void`\nDescription: Callback fired when the flip state changes.\n\n**`onCropChange`**\nType: `(details: CropChangeDetails) => void`\nDescription: Callback fired when the crop area changes.\n\n**`fixedCropArea`**\nType: `boolean`\nDescription: Whether the crop area is fixed in size and position.\n\n**`dir`**\nType: `\"ltr\" | \"rtl\"`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `() => ShadowRoot | Node | Document`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n### Machine API\n\nThe image cropper `api` exposes the following methods:\n\n**`zoom`**\nType: `number`\nDescription: The current zoom level of the image.\n\n**`rotation`**\nType: `number`\nDescription: The current rotation of the image in degrees.\n\n**`flip`**\nType: `FlipState`\nDescription: The current flip state of the image.\n\n**`crop`**\nType: `Rect`\nDescription: The current crop area rectangle in viewport coordinates.\n\n**`offset`**\nType: `Point`\nDescription: The current offset (pan position) of the image.\n\n**`naturalSize`**\nType: `Size`\nDescription: The natural (original) size of the image.\n\n**`viewportRect`**\nType: `BoundingRect`\nDescription: The viewport rectangle dimensions and position.\n\n**`dragging`**\nType: `boolean`\nDescription: Whether the crop area is currently being dragged.\n\n**`panning`**\nType: `boolean`\nDescription: Whether the image is currently being panned.\n\n**`setZoom`**\nType: `(zoom: number) => void`\nDescription: Function to set the zoom level of the image.\n\n**`zoomBy`**\nType: `(delta: number) => void`\nDescription: Function to zoom the image by a relative amount.\n\n**`setRotation`**\nType: `(rotation: number) => void`\nDescription: Function to set the rotation of the image.\n\n**`rotateBy`**\nType: `(degrees: number) => void`\nDescription: Function to rotate the image by a relative amount in degrees.\n\n**`setFlip`**\nType: `(flip: Partial<FlipState>) => void`\nDescription: Function to set the flip state of the image.\n\n**`flipHorizontally`**\nType: `(value?: boolean) => void`\nDescription: Function to flip the image horizontally. Pass a boolean to set explicitly or omit to toggle.\n\n**`flipVertically`**\nType: `(value?: boolean) => void`\nDescription: Function to flip the image vertically. Pass a boolean to set explicitly or omit to toggle.\n\n**`resize`**\nType: `(handlePosition: HandlePosition, delta: number) => void`\nDescription: Function to resize the crop area from a handle programmatically.\n\n**`reset`**\nType: `() => void`\nDescription: Function to reset the cropper to its initial state.\n\n**`getCroppedImage`**\nType: `(options?: GetCroppedImageOptions) => Promise<string | Blob>`\nDescription: Function to get the cropped image with all transformations applied.\nReturns a Promise that resolves to either a Blob or data URL.\n\n**`getCropData`**\nType: `() => CropData`\nDescription: Function to get the crop data in natural image pixel coordinates.\nThese coordinates are relative to the original image dimensions,\naccounting for zoom, rotation, and flip transformations.\nUse this for server-side cropping or state persistence.\n\n### Data Attributes\n\n**`Root`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: root\n**`data-fixed`**: \n**`data-shape`**: \n**`data-pinch`**: \n**`data-dragging`**: Present when in the dragging state\n**`data-panning`**: \n\n**`Viewport`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: viewport\n**`data-disabled`**: Present when disabled\n\n**`Image`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: image\n**`data-ready`**: \n**`data-flip-horizontal`**: \n**`data-flip-vertical`**: \n\n**`Selection`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: selection\n**`data-disabled`**: Present when disabled\n**`data-shape`**: \n**`data-measured`**: \n**`data-dragging`**: Present when in the dragging state\n**`data-panning`**: \n\n**`Handle`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: handle\n**`data-position`**: \n**`data-disabled`**: Present when disabled\n\n**`Grid`**\n\n**`data-scope`**: image-cropper\n**`data-part`**: grid\n**`data-axis`**: The axis to resize\n**`data-dragging`**: Present when in the dragging state\n**`data-panning`**: \n\n### CSS Variables\n\n<CssVarTable name=\"image-cropper\" />","package":"@zag-js/image-cropper","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/image-cropper.mdx"}