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/svelte # or yarn add @zag-js/image-cropper @zag-js/svelte

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 🔥

<script lang="ts"> import * as imageCropper from "@zag-js/image-cropper" import { normalizeProps, useMachine } from "@zag-js/svelte" const id = $props.id() const service = useMachine(imageCropper.machine, { id, }) const api = $derived(imageCropper.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <div {...api.getViewportProps()}> <img src="https://picsum.photos/seed/crop/640/400" crossorigin="anonymous" {...api.getImageProps()} /> <div {...api.getSelectionProps()}> {#each imageCropper.handles as position} <div {...api.getHandleProps({ position })}> <span /> </div> {/each} </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 }

Understanding coordinate systems

The image cropper uses two different coordinate systems:

1. Viewport Coordinates (api.crop)

These are the coordinates you see in the UI, relative to the visible viewport:

console.log(api.crop) // { x: 50, y: 30, width: 200, height: 150 }

Characteristics:

  • Relative to the viewport dimensions
  • Changes as you zoom and pan
  • Perfect for UI rendering and controls
  • Used by initialCrop and setCrop() (when implemented)

2. Natural Image Coordinates (api.getCropData())

These are the absolute pixel coordinates in the original image:

const cropData = api.getCropData() console.log(cropData) // { // x: 250, // y: 150, // width: 1000, // height: 750, // rotate: 0, // flipX: false, // flipY: false // }

Characteristics:

  • Relative to the original image dimensions
  • Independent of zoom/pan/viewport size
  • Essential for server-side cropping
  • Perfect for state persistence and undo/redo

When to use each

Use viewport coordinates (api.crop) when:

  • Rendering UI controls (sliders, displays)
  • Setting initial crop area
  • Building custom crop UI

Use natural coordinates (api.getCropData()) when:

  • Sending crop data to your backend for server-side processing
  • Persisting state (localStorage, database)
  • Implementing undo/redo functionality
  • Exporting crop configuration to external tools

Example: Server-side cropping

// Frontend: Get natural coordinates const cropData = api.getCropData() // Send to backend await fetch('/api/crop-image', { method: 'POST', body: JSON.stringify({ imageId: 'photo-123', crop: cropData, // Natural pixel coordinates }) }) // Backend: Crop the original image file // Use cropData.x, cropData.y, cropData.width, cropData.height // to crop the actual image file at full resolution

Transformation example

Here's how the coordinates relate with a zoom of 2x:

// Original image: 3000 × 2000 pixels // Viewport: 600 × 400 pixels // Zoom: 2x // Viewport coordinates (what you see) api.crop // { x: 100, y: 80, width: 200, height: 150 } // Natural coordinates (original image) api.getCropData() // { x: 500, y: 400, width: 1000, height: 750, ... } // Scale factor: 3000 / 600 = 5x // So 100px in viewport = 500px in original image

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:

  • zoomnumberThe current zoom level of the image.
  • rotationnumberThe current rotation of the image in degrees.
  • flipFlipStateThe current flip state of the image.
  • cropRectThe current crop area rectangle in viewport coordinates.
  • offsetPointThe current offset (pan position) of the image.
  • naturalSizeSizeThe natural (original) size of the image.
  • viewportRectBoundingRectThe viewport rectangle dimensions and position.
  • draggingbooleanWhether the crop area is currently being dragged.
  • panningbooleanWhether the image is currently being panned.
  • setZoom(zoom: number) => voidFunction to set the zoom level of the image.
  • zoomBy(delta: number) => voidFunction to zoom the image by a relative amount.
  • setRotation(rotation: number) => voidFunction to set the rotation of the image.
  • rotateBy(degrees: number) => voidFunction to rotate the image by a relative amount in degrees.
  • 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.
  • reset() => voidFunction to reset the cropper to its initial state.
  • 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.
  • getCropData() => CropDataFunction to get the crop data in natural image pixel coordinates. These coordinates are relative to the original image dimensions, accounting for zoom, rotation, and flip transformations. Use this for server-side cropping or state persistence.

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
data-dragging
Present when in the dragging state
Handle
data-scope
image-cropper
data-part
handle
data-disabled
Present when disabled
Grid
data-scope
image-cropper
data-part
grid
data-axis
The axis to resize
data-dragging
Present when in the dragging state

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
--image-zoom
The image zoom value for the Root
--image-rotation
The image rotation value for the Root
--image-offset-x
The offset position for image
--image-offset-y
The offset position for image
Edit this page on GitHub