Skip to content

Conversation

nelsonlaidev
Copy link
Collaborator

@nelsonlaidev nelsonlaidev commented Sep 28, 2025

Image Cropper

Overview

This PR introduces a new Image Cropper machine component to the Zag.js library. The image cropper is a comprehensive, headless state machine that provides full-featured image cropping functionality with support for zoom, rotation, flipping, precise crop area manipulation, and image export capabilities.

Features

Core Functionality

  • Crop Area Management: Drag to move, resize from 8 handles (corners and edges)
  • Zoom Control: Mouse wheel, keyboard shortcuts (+/-), and pinch-to-zoom gestures
  • Image Rotation: Set rotation in degrees (0-360°)
  • Image Flipping: Horizontal and vertical flip capabilities
  • Aspect Ratio Locking: Maintain fixed aspect ratios during resizing
  • Circular & Rectangle Crop Shapes: Support for both crop area shapes
  • Image Export: Export cropped image with all transformations applied as Blob or Data URL

Interaction Methods

  • Mouse/Pointer: Drag to pan and resize, wheel to zoom
  • Touch: Pinch-to-zoom with smooth scaling and panning
  • Keyboard:
    • Arrow keys to move crop area
    • Alt + Arrow keys to resize
    • +/- keys to zoom
    • Shift/Ctrl modifiers for larger increments

Advanced Features

  • Fixed Crop Area: Lock crop dimensions and position while allowing image panning
  • Smart Default Sizing: Automatically computes optimal crop dimensions based on viewport and aspect ratio
  • Configurable Size Limits: Min/max width and height constraints
  • Customizable Nudge Steps: Different step sizes for normal, shift, and ctrl key modifiers
  • Zoom Sensitivity: Adjustable pinch-to-zoom responsiveness
  • Image Export Options: Export with custom format (PNG, JPEG, etc.), quality settings, and output type (Blob or Data URL)
  • Accessibility: Full ARIA labels, descriptions, and keyboard navigation support

Architecture

File Structure

State Machine

The machine operates in three states:

  • idle: Waiting for user interaction
  • dragging: Actively resizing or moving the crop area
  • panning: Panning the image within the viewport

Key Algorithms

  • Aspect Ratio Preservation: Complex clamping logic ensures aspect ratios are maintained during resize from any handle
  • Offset Clamping: Calculates rotation-aware bounding boxes to constrain image panning
  • Keyboard Crop Manipulation: Handles corner, edge, and side-specific keyboard resize operations
  • Pinch Zoom: Smooth scaling with optional panning based on touch midpoint movement
  • Image Export: Canvas-based rendering that applies all transformations (zoom, rotation, flip) and extracts the cropped region with proper coordinate mapping

Configuration Options

Size & Constraints

  • minWidth, minHeight: Minimum crop dimensions (default: 40px)
  • maxWidth, maxHeight: Maximum crop dimensions (default: Infinity)
  • aspectRatio: Optional fixed aspect ratio (width/height)
  • initialCrop: Custom initial crop rectangle

Zoom & Transform

  • defaultZoom: Initial zoom level (default: 1)
  • minZoom, maxZoom: Zoom range (default: 1-5)
  • zoomStep: Increment per wheel step (default: 0.1)
  • zoomSensitivity: Pinch gesture responsiveness (default: 2)
  • defaultRotation: Initial rotation in degrees (default: 0)
  • defaultFlip: Initial flip state (default: no flip)

Behavior

  • cropShape: "rectangle" or "circle" (default: "rectangle")
  • fixedCropArea: Lock crop position/size while panning image (default: false)
  • nudgeStep, nudgeStepShift, nudgeStepCtrl: Keyboard movement increments

Callbacks

  • onZoomChange: Fired when zoom level changes
  • onRotationChange: Fired when rotation changes
  • onFlipChange: Fired when flip state changes
  • onCropChange: Fired when the crop area changes

API Methods

The ImageCropperApi provides:

  • setZoom(zoom): Set zoom level programmatically
  • setRotation(rotation): Set rotation in degrees
  • setFlip(flip): Set flip state
  • flipHorizontally(value?): Toggle or set horizontal flip
  • flipVertically(value?): Toggle or set vertical flip
  • resize(handlePosition, delta): Resize crop area from a handle
  • getCroppedImage(options?): Export the cropped image with all transformations applied
    • Options: type (image format), quality (0-1), output ("blob" or "dataUrl")
    • Returns: Promise resolving to Blob or data URL string
  • getRootProps(), getViewportProps(), getImageProps(), getSelectionProps(), getHandleProps(): Prop getters for DOM elements

Image Export

The getCroppedImage method provides powerful export capabilities:

// Export as PNG Blob (default)
const blob = await api.getCroppedImage()

// Export as Data URL
const dataUrl = await api.getCroppedImage({ output: "dataUrl" })

// Export as JPEG with custom quality
const jpegBlob = await api.getCroppedImage({ 
  type: "image/jpeg", 
  quality: 0.8 
})

The export functionality:

  • Applies all transformations (zoom, rotation, flip) to the output
  • Correctly maps viewport coordinates to natural image coordinates
  • Supports any image format supported by the browser's canvas API
  • Handles CORS requirements (requires crossOrigin="anonymous" on image element)

Accessibility

  • Full ARIA support with roles, labels, and descriptions
  • Keyboard navigation with arrow keys and modifiers
  • Live region updates for state changes
  • Semantic HTML structure with proper element relationships

Testing

Comprehensive E2E tests included in e2e/image-cropper.e2e.ts with test model in e2e/models/image-cropper.model.ts.

Demo

The example implementation in examples/next-ts/pages/image-cropper.tsx showcases all features including:

  • Interactive crop area manipulation
  • Zoom, rotation, and flip controls
  • Programmatic resize functionality
  • Image export with preview - Export as Blob, Data URL, or download as PNG file

Copy link

vercel bot commented Sep 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
zag-nextjs Ready Ready Preview Oct 17, 2025 11:16am
zag-solid Ready Ready Preview Oct 17, 2025 11:16am
zag-svelte Ready Ready Preview Oct 17, 2025 11:16am
zag-vue Ready Ready Preview Oct 17, 2025 11:16am
zag-website Ready Ready Preview Oct 17, 2025 11:16am

Copy link

changeset-bot bot commented Sep 28, 2025

🦋 Changeset detected

Latest commit: a1c1bd2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 80 packages
Name Type
@zag-js/image-cropper Patch
@zag-js/anatomy-icons Patch
@zag-js/anatomy Patch
@zag-js/core Patch
@zag-js/docs Patch
@zag-js/preact Patch
@zag-js/react Patch
@zag-js/solid Patch
@zag-js/svelte Patch
@zag-js/vue Patch
@zag-js/accordion Patch
@zag-js/angle-slider Patch
@zag-js/async-list Patch
@zag-js/avatar Patch
@zag-js/bottom-sheet Patch
@zag-js/carousel Patch
@zag-js/checkbox Patch
@zag-js/clipboard Patch
@zag-js/collapsible Patch
@zag-js/color-picker Patch
@zag-js/combobox Patch
@zag-js/date-picker Patch
@zag-js/dialog Patch
@zag-js/editable Patch
@zag-js/file-upload Patch
@zag-js/floating-panel Patch
@zag-js/hover-card Patch
@zag-js/listbox Patch
@zag-js/menu Patch
@zag-js/navigation-menu Patch
@zag-js/number-input Patch
@zag-js/pagination Patch
@zag-js/password-input Patch
@zag-js/pin-input Patch
@zag-js/popover Patch
@zag-js/presence Patch
@zag-js/progress Patch
@zag-js/qr-code Patch
@zag-js/radio-group Patch
@zag-js/rating-group Patch
@zag-js/scroll-area Patch
@zag-js/select Patch
@zag-js/signature-pad Patch
@zag-js/slider Patch
@zag-js/splitter Patch
@zag-js/steps Patch
@zag-js/switch Patch
@zag-js/tabs Patch
@zag-js/tags-input Patch
@zag-js/timer Patch
@zag-js/toast Patch
@zag-js/toggle-group Patch
@zag-js/toggle Patch
@zag-js/tooltip Patch
@zag-js/tour Patch
@zag-js/tree-view Patch
@zag-js/store Patch
@zag-js/types Patch
@zag-js/aria-hidden Patch
@zag-js/auto-resize Patch
@zag-js/collection Patch
@zag-js/color-utils Patch
@zag-js/utils Patch
@zag-js/date-utils Patch
@zag-js/dismissable Patch
@zag-js/dom-query Patch
@zag-js/file-utils Patch
@zag-js/focus-trap Patch
@zag-js/focus-visible Patch
@zag-js/highlight-word Patch
@zag-js/hotkeys Patch
@zag-js/i18n-utils Patch
@zag-js/interact-outside Patch
@zag-js/json-tree-utils Patch
@zag-js/live-region Patch
@zag-js/popper Patch
@zag-js/rect-utils Patch
@zag-js/remove-scroll Patch
@zag-js/scroll-snap Patch
@zag-js/stringify-state Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants