Skip to main content

Image Cropper

The image cropper machine keeps track of the crop rectangle, zoom, rotation, pan offset, flip state, and every gesture required to edit them. It exposes a set of DOM props so you can render your own viewport, frame, and handles in any framework.

Features

  • Pointer, wheel, and pinch gestures that pan, zoom, rotate, and flip the image
  • Handles that resize the crop area with snapping, aspect-ratio locking, and keyboard nudges
  • Supports rectangular or circular crops, fixed crop windows, and constrained min/max dimensions
  • Fully controllable zoom/rotation/flip values with change callbacks
  • Programmatic helpers such as api.resize and api.getCroppedImage
  • Accessible slider semantics, custom translations, and data attributes for styling

Installation

To use the image cropper machine in your project, run the following command in your command line:

npm install @zag-js/image-cropper @zag-js/react # or yarn add @zag-js/image-cropper @zag-js/react

Anatomy

To set up the image cropper correctly, you'll need to understand its anatomy and how we name its parts.

Each part includes a data-part attribute to help identify them in the DOM.

No anatomy available for image-cropper

Usage

First, import the image cropper package into your project:

import * as imageCropper from "@zag-js/image-cropper"

The package exports two key functions:

  • machine — The state machine logic for the cropper.
  • connect — The function that translates the machine's state to JSX attributes and event handlers.

Next, import the required hooks and functions for your framework and use the image cropper machine in your project 🔥

import * as imageCropper from "@zag-js/image-cropper" import { normalizeProps, useMachine } from "@zag-js/react" import { useId } from "react" export function ImageCropper() { const service = useMachine(imageCropper.machine, { id: useId(), }) const api = imageCropper.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <div {...api.getViewportProps()}> <img src="https://picsum.photos/seed/crop/640/400" crossOrigin="anonymous" {...api.getImageProps()} /> <div {...api.getSelectionProps()}> {imageCropper.handles.map((position) => ( <div key={position} {...api.getHandleProps({ position })}> <span /> </div> ))} </div> </div> </div> ) }

Setting the initial crop

Pass an initialCrop to start from a specific rectangle. The size is constrained to your min/max and viewport, and the position is clamped within the viewport.

const service = useMachine(imageCropper.machine, { initialCrop: { x: 40, y: 40, width: 240, height: 240 }, aspectRatio: 1, // optional, lock to square }) const api = imageCropper.connect(service, normalizeProps)

Fixed crop area

Lock the crop window and allow only panning/zooming of the image beneath it by setting fixedCropArea: true.

const service = useMachine(imageCropper.machine, { fixedCropArea: true, })

Crop shape and aspect ratio

  • cropShape can be "rectangle" or "circle".
  • aspectRatio can lock the crop to a width/height ratio. When aspectRatio is not set and cropShape is "rectangle", holding Shift while resizing locks to the current ratio.
const service = useMachine(imageCropper.machine, { cropShape: "circle", aspectRatio: 1, // ignored for circle })

Controlling zoom, rotation, and flip

You can configure defaults and limits, and also control them programmatically using the API.

const service = useMachine(imageCropper.machine, { defaultZoom: 1.25, minZoom: 1, maxZoom: 5, defaultRotation: 0, defaultFlip: { horizontal: false, vertical: false }, }) const api = imageCropper.connect(service, normalizeProps) // Programmatic controls api.setZoom(2) // zoom to 2x api.setRotation(90) // rotate to 90 degrees api.flipHorizontally() // toggle horizontal flip api.setFlip({ vertical: true }) // set vertical flip on

Programmatic resizing

Use api.resize(handle, delta) to resize from any handle programmatically. Positive delta grows outward, negative shrinks inward.

// Grow the selection by 8px from the right edge api.resize("right", 8) // Shrink from top-left corner by 4px in both axes api.resize("top-left", -4)

Getting the cropped image

Use api.getCroppedImage to export the current crop, taking zoom/rotation/flip/pan into account.

// Blob (default) const blob = await api.getCroppedImage({ type: "image/png", quality: 0.92 }) // Data URL const dataUrl = await api.getCroppedImage({ output: "dataUrl", type: "image/jpeg", quality: 0.85, }) // Example usage if (blob) { const url = URL.createObjectURL(blob) previewImg.src = url }

Touch and wheel gestures

  • Use the mouse wheel over the viewport to zoom at the pointer location.
  • Pinch with two fingers to zoom and pan; the machine smooths tiny changes and tracks the pinch midpoint.
  • Drag on the viewport background to pan the image (when not dragging the selection).

Keyboard nudges

Configure keyboard nudge steps for move/resize:

const service = useMachine(imageCropper.machine, { nudgeStep: 1, nudgeStepShift: 10, nudgeStepCtrl: 50, })

Accessibility

  • The root is a live region with helpful descriptions of crop, zoom, and rotation status.
  • The selection exposes slider-like semantics to assistive tech and supports keyboard movement, resizing (Alt+Arrows), and zooming (+/-).
  • Customize accessible labels and descriptions via translations:
const service = useMachine(imageCropper.machine, { translations: { rootLabel: "Product image cropper", selectionInstructions: "Use arrow keys to move, Alt+arrows to resize, and +/- to zoom.", }, })

Styling guide

Earlier, we mentioned that each image cropper part has a data-part attribute added to them to select and style them in the DOM.

[data-scope="image-cropper"][data-part="root"] { /* styles for the root part */ } [data-scope="image-cropper"][data-part="viewport"] { /* styles for the viewport part */ } [data-scope="image-cropper"][data-part="image"] { /* styles for the image part */ } [data-scope="image-cropper"][data-part="selection"] { /* styles for the selection part */ } [data-scope="image-cropper"][data-part="handle"] { /* styles for the handle part */ }

Selection shapes

The selection can be styled based on its shape:

[data-part="selection"][data-shape="circle"] { /* styles for circular selection */ } [data-part="selection"][data-shape="rectangle"] { /* styles for rectangular selection */ }

States

Various states can be styled using data attributes:

[data-part="root"][data-dragging] { /* styles when dragging the selection */ } [data-part="root"][data-fixed] { /* styles when the crop area is fixed */ }

Keyboard Interactions

  • ArrowUp
    Moves the crop selection upward by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.
  • ArrowDown
    Moves the crop selection downward by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.
  • ArrowLeft
    Moves the crop selection to the left by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.
  • ArrowRight
    Moves the crop selection to the right by the configured nudge step. Hold Shift for the `nudgeStepShift` value or Ctrl/Cmd for `nudgeStepCtrl`.
  • Alt + ArrowUp
    Resizes the crop vertically from the bottom handle, reducing the height. Hold Shift or Ctrl/Cmd for the larger nudge steps.
  • Alt + ArrowDown
    Resizes the crop vertically from the bottom handle, increasing the height. Hold Shift or Ctrl/Cmd for the larger nudge steps.
  • Alt + ArrowLeft
    Resizes the crop horizontally from the right handle, reducing the width. Hold Shift or Ctrl/Cmd for the larger nudge steps.
  • Alt + ArrowRight
    Resizes the crop horizontally from the right handle, increasing the width. Hold Shift or Ctrl/Cmd for the larger nudge steps.
  • +
    Zooms in on the image. The `=` key performs the same action on keyboards where both symbols share a key.
  • -
    Zooms out of the image. The `_` key performs the same action on keyboards where both symbols share a key.

Methods and Properties

Machine Context

The image cropper machine exposes the following context properties:

  • idsPartial<{ root: string; viewport: string; image: string; selection: string; handle: (position: string) => string; }>The ids of the image cropper elements
  • translationsIntlTranslationsSpecifies the localized strings that identify accessibility elements and their states.
  • initialCropRectThe initial rectangle of the crop area. If not provided, a smart default will be computed based on viewport size and aspect ratio.
  • minWidthnumberThe minimum width of the crop area
  • minHeightnumberThe minimum height of the crop area
  • maxWidthnumberThe maximum width of the crop area
  • maxHeightnumberThe maximum height of the crop area
  • aspectRationumberThe aspect ratio to maintain for the crop area (width / height). For example, an aspect ratio of 16 / 9 will maintain a width to height ratio of 16:9. If not provided, the crop area can be freely resized.
  • cropShape"rectangle" | "circle"The shape of the crop area.
  • zoomnumberThe controlled zoom level of the image.
  • rotationnumberThe controlled rotation of the image in degrees (0 - 360).
  • flipFlipStateThe controlled flip state of the image.
  • defaultZoomnumberThe initial zoom factor to apply to the image.
  • defaultRotationnumberThe initial rotation to apply to the image in degrees.
  • defaultFlipFlipStateThe initial flip state to apply to the image.
  • zoomStepnumberThe amount of zoom applied per wheel step.
  • zoomSensitivitynumberControls how responsive pinch-to-zoom is.
  • minZoomnumberThe minimum zoom factor allowed.
  • maxZoomnumberThe maximum zoom factor allowed.
  • nudgeStepnumberThe base nudge step for keyboard arrow keys (in pixels).
  • nudgeStepShiftnumberThe nudge step when Shift key is held (in pixels).
  • nudgeStepCtrlnumberThe nudge step when Ctrl/Cmd key is held (in pixels).
  • onZoomChange(details: ZoomChangeDetails) => voidCallback fired when the zoom level changes.
  • onRotationChange(details: RotationChangeDetails) => voidCallback fired when the rotation changes.
  • onFlipChange(details: FlipChangeDetails) => voidCallback fired when the flip state changes.
  • onCropChange(details: CropChangeDetails) => voidCallback fired when the crop area changes.
  • fixedCropAreabooleanWhether the crop area is fixed in size and position.
  • dir"ltr" | "rtl"The document's text/writing direction.
  • idstringThe unique identifier of the machine.
  • getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.

Machine API

The image cropper api exposes the following methods:

  • setZoom(zoom: number) => voidFunction to set the zoom level of the image.
  • setRotation(rotation: number) => voidFunction to set the rotation of the image.
  • setFlip(flip: Partial<FlipState>) => voidFunction to set the flip state of the image.
  • flipHorizontally(value?: boolean) => voidFunction to flip the image horizontally. Pass a boolean to set explicitly or omit to toggle.
  • flipVertically(value?: boolean) => voidFunction to flip the image vertically. Pass a boolean to set explicitly or omit to toggle.
  • resize(handlePosition: HandlePosition, delta: number) => voidFunction to resize the crop area from a handle programmatically.
  • getCroppedImage(options?: GetCroppedImageOptions) => Promise<string | Blob>Function to get the cropped image with all transformations applied. Returns a Promise that resolves to either a Blob or data URL.

Data Attributes

Root
data-scope
image-cropper
data-part
root
data-dragging
Present when in the dragging state
Viewport
data-scope
image-cropper
data-part
viewport
data-disabled
Present when disabled
Image
data-scope
image-cropper
data-part
image
Selection
data-scope
image-cropper
data-part
selection
data-disabled
Present when disabled
Handle
data-scope
image-cropper
data-part
handle
data-disabled
Present when disabled

CSS Variables

Root
--crop-width
The width of the Root
--crop-height
The height of the Root
--crop-x
The crop x value for the Root
--crop-y
The crop y value for the Root
Selection
--width
The width of the element
--height
The height of the element
--x
The x position for transform
--y
The y position for transform
Edit this page on GitHub