CropModal

The CropModal is a comprehensive image cropping solution designed for modern web applications. It provides a complete workflow for image selection, cropping, processing, and upload with features including responsive design, accessibility, and robust error handling.

Quick Start

Installation
npm install @adaptavant/crop-modal
Import
import { CropModal } from '@adaptavant/crop-modal';

Key Functionalities​

🎯 Flexible Cropping Options​

  • Aspect Ratio Control: Fixed ratios (3:1 for banners, 1:1 for profile or service images)
  • Shape Variants: Rectangle, rounded corners, or circles
  • Zoom & Pan: Intuitive zoom controls with mouse wheel and touch support
  • Precision Controls: Fine-grained positioning with visual guides

πŸ”„ Dual Operation Modes​

  • Offline Mode: Local processing with immediate preview and blob generation
  • Online Mode: Direct upload to cloud storage with API integration
  • Hybrid Workflow: Local image processing followed by remote upload

πŸ“± Responsive Design​

  • Mobile-First: Touch-optimized controls and gestures
  • Adaptive UI: Automatic layout adjustments for different screen sizes

πŸ›‘οΈ Security​

  • File Type Validation: Configurable MIME type restrictions
  • Size Limits: Customizable file size constraints
  • Image Processing: Automatic compression and resizing
  • Error Boundaries: Comprehensive error handling and user feedback

Additional Exports​

The crop-modal package exports additional components and utilities beyond the main CropModal component:

Components​

  • UploadFileButton - A wrapper component that provides file upload functionality to any button using the render props pattern

Utility Functions​

The package includes a comprehensive set of utility functions for image processing and validation:

  • clamp - Restricts numeric values within a specified range
  • compressImage - Compresses image blobs using Compressor.js
  • createImageElement - Creates HTMLImageElement from string sources
  • createUrl - Safely constructs URL instances with error handling
  • genericSendFormDataRequest - Sends multipart/form-data POST requests
  • isFileSizeValid - Validates file sizes against limits
  • parseImageProcessingOptions - Merges image processing options with defaults
  • processLocalFile - Complete local file processing pipeline with validation, resize, and compression
  • readFileImage - Converts blobs to data URLs or ArrayBuffers
  • resizeImage - Resizes images while maintaining aspect ratio
  • roundNumberToDecimal - Rounds numbers to specified decimal places
  • validateImageType - Validates files against accepted MIME types

TypeScript Types​

All TypeScript interfaces and types are exported, including CropModalProps, ImageProcessingOptions, ApiOptions, translation interfaces, and many utility types for comprehensive type safety.

Default​

This example demonstrates the basic usage of CropModal with minimal configuration:

Workflow:

  1. User clicks "Open Crop Modal" to launch the cropping interface
  2. User uploads and crops an image within the modal
  3. On save, the modal closes and the cropped image appears in the avatar
  4. User can clear the image to reset the state
const [isOpen, setIsOpen] = React.useState(false);
const [imageUrl, setImageUrl] = React.useState(null);

const handleClose = React.useCallback(() => setIsOpen(false), []);
const handleSave = React.useCallback((newUrl) => {
  setIsOpen(false);
  setImageUrl(newUrl);
}, []);

return (
  <Box>
    <Avatar name="Default" size="96">
      {imageUrl ? <AvatarImage src={imageUrl} /> : null}
    </Avatar>
    <CropModal
      cropViewBoxOptions={{}}
      dropAreaTranslations={{
        uploadImageButtonLabel: 'Upload',
      }}
      imageUrl={imageUrl}
      isOpen={isOpen}
      modalWrapperTranslations={{
        title: 'Default story',
        subtitle: 'That`s what you get with the default/minimal configuration',
        saveButtonLabel: 'Save',
        cancelButtonLabel: 'Cancel',
        closeModalButtonLabel: 'Close crop modal',
      }}
      onCancel={handleClose}
      onClose={handleClose}
      onSuccessfulUpload={handleSave}
    />
    <Box className="flex mt-2 justify-center items-center gap-2">
      <Button onClick={() => setIsOpen(true)}>Open Crop Modal</Button>
      <Button isDisabled={!imageUrl} onClick={() => setImageUrl(null)}>
        Clear image
      </Button>
    </Box>
  </Box>
);

This example demonstrates a complete end-to-end banner image workflow with advanced configuration for a 3:1 aspect ratio banner:

Advanced Workflow:

  1. Initial State: Empty banner area displays placeholder icon with upload button
  2. File Selection: User selects image through file input with MIME type validation
  3. Pre-processing: Image undergoes local processing (resize, compression, validation) before cropping
  4. Crop Modal: Opens with pre-processed image and banner-specific constraints
  5. Final Processing: User crops image within 3:1 aspect ratio constraints
  6. Result Display: Processed banner image displays in the container
  7. Management Options: Edit button reopens crop modal, delete button clears the image
const IMAGE_PROCESSING_OPTIONS = parseImageProcessingOptions();

const [isOpen, setIsOpen] = React.useState(false);
const [imageUrl, setImageUrl] = React.useState(null);
const [uploadedBlobUrl, setUploadedBlobUrl] = React.useState(null);

const handleClose = React.useCallback(() => setIsOpen(false), []);
const handleSave = React.useCallback((newUrl) => {
  setIsOpen(false);
  setImageUrl(newUrl);
  setUploadedBlobUrl(null);
}, []);

const handleEditImage = React.useCallback(() => setIsOpen(true), []);
const handleDelete = React.useCallback(() => setImageUrl(null), []);

const handleFileInputChange = React.useCallback(async (files) => {
  const { blob, errorMessage } = await processLocalFile(
    files[0],
    undefined,
    IMAGE_PROCESSING_OPTIONS
  );

  if (!blob) {
    window.alert(errorMessage);
    return;
  }

  setIsOpen(true);
  setUploadedBlobUrl(URL.createObjectURL(blob));
}, []);

return (
  <Box className="w-full">
    <Box className="bg-neutral-secondary rounded-8px after:rounded-8px relative flex aspect-[3/1] h-auto w-auto max-w-full items-center justify-center overflow-hidden after:pointer-events-none after:absolute after:inset-0 after:border after:border-solid after:border-[rgba(0,0,0,0.05)] after:content-['']">
      {!imageUrl ? (
        <SetmoreIcon />
      ) : (
        <Image
          alt="Banner"
          className="absolute inset-0 h-full w-full object-cover"
          src={imageUrl}
        />
      )}
      <Box className="absolute bottom-3 right-3 flex gap-1">
        {!imageUrl && (
          <UploadFileButton
            accept={IMAGE_PROCESSING_OPTIONS.allowedMimeType}
            onFilesChange={handleFileInputChange}
          >
            {({ onClick }) => (
              <Button iconStart={ImageIcon} onClick={onClick} variant="neutralSecondary">
                Upload banner image
              </Button>
            )}
          </UploadFileButton>
        )}
        {!!imageUrl && (
          <>
            <Button iconStart={EditIcon} onClick={handleEditImage} variant="neutralSecondary">
              Edit
            </Button>
            <Tooltip content="Delete" placement="bottom">
              {({ triggerProps }) => (
                <IconButton
                  aria-label="Delete Banner Image"
                  variant="neutralSecondary"
                  {...triggerProps}
                  icon={DeleteIcon}
                  onClick={handleDelete}
                />
              )}
            </Tooltip>
          </>
        )}
      </Box>
    </Box>
    <CropModal
      cropAreaTranslations={{
        uploadNewButtonLabel: 'Upload new',
        zoomInButtonLabel: 'Zoom in',
        zoomOutButtonLabel: 'Zoom out',
      }}
      cropViewBoxOptions={{
        width: 500,
        aspectRatio: 3 / 1,
      }}
      imageProcessingOptions={IMAGE_PROCESSING_OPTIONS}
      imageUrl={uploadedBlobUrl ?? imageUrl}
      isOpen={isOpen}
      modalWrapperTranslations={{
        title: 'Banner image',
        subtitle: '1200 x 400 px recommended Β· JPG, JPEG and PNG files only Β· Up to 5MB',
        saveButtonLabel: 'Save',
        cancelButtonLabel: 'Cancel',
        closeModalButtonLabel: 'Close crop modal',
      }}
      onCancel={handleClose}
      onClose={handleClose}
      onSuccessfulUpload={handleSave}
    />
  </Box>
);

This example demonstrates a comprehensive brand logo management system circular cropping functionality:

Workflow:

  1. Initial State: Empty circular placeholder with brand icon and upload button
  2. File Selection: User selects logo image through file input with validation
  3. Pre-processing: Image undergoes local processing (resize, compression, validation) before cropping
  4. Circular Crop Modal: Opens with pre-processed image and circular crop constraints
  5. Logo Cropping: User adjusts image within circular boundaries with zoom controls
  6. Final Processing: Cropped circular logo is processed and displayed
  7. Management Options: Edit button reopens crop modal, delete button clears the logo

Brand Identity Focus: This implementation is specifically designed for brand logo management, with circular cropping that's ideal for:

  • Company logos and brand marks
  • Profile pictures and avatars
Brand logoSelect a 200 x 200 px image, up to 10MB in size
const IMAGE_PROCESSING_OPTIONS = parseImageProcessingOptions({
  maxWidth: 1024,
  maxHeight: 1024,
});

const [isOpen, setIsOpen] = React.useState(false);
const [imageUrl, setImageUrl] = React.useState(null);
const [uploadedBlobUrl, setUploadedBlobUrl] = React.useState(null);

const handleClose = React.useCallback(() => setIsOpen(false), []);
const handleSave = React.useCallback((newUrl) => {
  setIsOpen(false);
  setImageUrl(newUrl);
  setUploadedBlobUrl(null);
}, []);

const handleEditImage = React.useCallback(() => setIsOpen(true), []);
const handleDelete = React.useCallback(() => setImageUrl(null), []);

const handleFileInputChange = React.useCallback(async (files) => {
  const { blob, errorMessage } = await processLocalFile(
    files[0],
    undefined,
    IMAGE_PROCESSING_OPTIONS
  );

  if (!blob) {
    window.alert(errorMessage);
    return;
  }

  setIsOpen(true);
  setUploadedBlobUrl(URL.createObjectURL(blob));
}, []);

return (
  <Box>
    <Box className="flex gap-4">
      <Box className="relative inline-flex h-20 w-20 min-w-20 items-center justify-center overflow-hidden rounded-full after:absolute after:inset-0 after:rounded-full after:border after:border-solid after:border-[rgba(0,0,0,0.05)]">
        {imageUrl ? (
          <Image
            alt="Brand logo"
            className="bg-neutral-secondary h-full w-full object-cover"
            src={imageUrl}
          />
        ) : (
          <Box className="bg-neutral-secondary flex h-full w-full items-center justify-center">
            <SetmoreIcon />
          </Box>
        )}
      </Box>
      <Box className="flex flex-col">
        <Text className="text-body-12 text-primary">Brand logo</Text>
        <Text className="text-body-12 text-secondary">
          Select a 200 x 200 px image, up to 10MB in size
        </Text>
        <Box className="mt-1 flex gap-1">
          {imageUrl ? (
            <>
              <Button
                iconStart={EditIcon}
                onClick={handleEditImage}
                variant="neutralSecondary"
              >
                Edit
              </Button>
              <Tooltip content="Delete" placement="bottom">
                {({ triggerProps }) => (
                  <IconButton
                    aria-label="Delete brand logo"
                    variant="neutralTertiary"
                    {...triggerProps}
                    icon={DeleteIcon}
                    onClick={handleDelete}
                  />
                )}
              </Tooltip>
            </>
          ) : (
            <UploadFileButton accept={IMAGE_PROCESSING_OPTIONS.allowedMimeType} onFilesChange={handleFileInputChange}>
              {({ onClick }) => (
                <Button iconStart={ImageIcon} onClick={onClick} variant="neutralSecondary">Upload logo</Button>
              )}
            </UploadFileButton>
          )}
        </Box>
      </Box>
    </Box>
    <CropModal
      cropAreaTranslations={{
        uploadNewButtonLabel: 'Upload new',
        zoomInButtonLabel: 'Zoom in',
        zoomOutButtonLabel: 'Zoom out',
      }}
      cropViewBoxOptions={{ roundingStyle: 'circle' }}
      imageProcessingOptions={IMAGE_PROCESSING_OPTIONS}
      imageUrl={uploadedBlobUrl ?? imageUrl}
      isOpen={isOpen}
      modalWrapperTranslations={{
        title: 'Brand logo',
        subtitle: '200 x 200 px recommended Β· JPG, JPEG and PNG files only Β· Up to 10MB',
        saveButtonLabel: 'Save',
        cancelButtonLabel: 'Cancel',
        closeModalButtonLabel: 'Close crop modal',
      }}
      onCancel={handleClose}
      onClose={handleClose}
      onSuccessfulUpload={handleSave}
    />
  </Box>
);

Service Image​

This example demonstrates a comprehensive service image management system with rounded corner cropping and online upload capabilities:

Workflow:

  1. Initial State: Empty rounded placeholder with service icon and upload button
  2. File Selection: User selects service image through file input with validation
  3. Pre-processing: Image undergoes local processing (resize, compression, validation) before upload
  4. Rounded Crop Modal: Opens with pre-processed image and rounded corner constraints
  5. Image Cropping: User adjusts image within rounded boundaries with zoom controls
  6. Online Upload: Processed image is uploaded to server via configured API endpoint
  7. Final Processing: Server-processed image URL is returned and displayed
  8. Management Options: Edit button reopens crop modal, delete button clears the image

Service Image Focus: This implementation is specifically designed for service image management, with rounded corner cropping that's ideal for:

  • Service catalog images
  • Product thumbnails
Service imageUp to 5 MB in size
const IMAGE_PROCESSING_OPTIONS = parseImageProcessingOptions({
  maxWidth: 1024,
  maxHeight: 1024
});

const API_OPTIONS = React.useMemo(() => ({
  uploadUrl: 'https://example.com/upload-session',
  getAccessToken: async () => 'mock-token',
}), []);

const [isOpen, setIsOpen] = React.useState(false);
const [imageUrl, setImageUrl] = React.useState(null);
const [uploadedBlobUrl, setUploadedBlobUrl] = React.useState(null);

const handleClose = React.useCallback(() => setIsOpen(false), []);
const handleSave = React.useCallback((newUrl) => {
  setIsOpen(false);
  setImageUrl(newUrl);
  setUploadedBlobUrl(null);
}, []);

const handleEditImage = React.useCallback(() => setIsOpen(true), []);
const handleDelete = React.useCallback(() => setImageUrl(null), []);

const handleFileInputChange = React.useCallback(async (files) => {
  const { blob, errorMessage } = await processLocalFile(
    files[0],
    undefined,
    IMAGE_PROCESSING_OPTIONS
  );

  if (!blob) {
    window.alert(errorMessage);
    return;
  }

  setIsOpen(true);
  setUploadedBlobUrl(URL.createObjectURL(blob));
}, []);

return (
  <Box>
    <Box className="flex items-center gap-4">
      <Box className="relative inline-flex h-24 w-24 min-w-24 items-center justify-center overflow-hidden rounded-16px after:absolute after:inset-0 after:rounded-16px after:border after:border-solid after:border-[rgba(0,0,0,0.05)]">
        {imageUrl ? (
          <Image
            src={imageUrl}
            alt="Service image"
            className="bg-neutral-secondary h-full w-full object-cover"
          />
        ) : (
          <Box className="bg-neutral-secondary flex h-full w-full items-center justify-center">
            <SetmoreIcon />
          </Box>
        )}
      </Box>
      <Box className="flex flex-col">
        <Text className="text-body-12 text-primary">Service image</Text>
        <Text className="text-body-12 text-secondary">Up to 5 MB in size</Text>
        <Box className="mt-1 flex gap-1">
          {imageUrl ? (
            <>
              <Button iconStart={EditIcon} onClick={handleEditImage} variant="neutralSecondary">
                Edit
              </Button>
              <Tooltip content="Delete" placement="bottom">
                {({ triggerProps }) => (
                  <IconButton aria-label="Delete service image" variant="neutralTertiary" {...triggerProps} icon={DeleteIcon} onClick={handleDelete} />
                )}
              </Tooltip>
            </>
          ) : (
            <UploadFileButton accept={IMAGE_PROCESSING_OPTIONS.allowedMimeType} onFilesChange={handleFileInputChange}>
              {({ onClick }) => (
                <Button iconStart={ImageIcon} onClick={onClick} variant="neutralSecondary">
                  Upload
                </Button>
              )}
            </UploadFileButton>
          )}
        </Box>
      </Box>
    </Box>
    <CropModal
      apiOptions={API_OPTIONS}
      cropAreaTranslations={{
        uploadNewButtonLabel: 'Upload new',
        zoomInButtonLabel: 'Zoom in',
        zoomOutButtonLabel: 'Zoom out',
      }}
      cropViewBoxOptions={{ roundingStyle: 'rounded' }}
      errorTranslations={{
        androidFileReadError: 'Custom error message for Android file read error',
        fetchIncomingImageError: 'Custom error message for fetch incoming image error',
        fileSizeExceedsLimitError: 'Custom error message for file size exceeds limit error',
        fileTypeMismatchError: 'Custom error message for file type mismatch error',
        fileUploadError: 'Custom error message for file upload error',
        localFileProcessingError: 'Custom error message for local file processing error',
      }}
      imageProcessingOptions={IMAGE_PROCESSING_OPTIONS}
      imageUrl={uploadedBlobUrl ?? imageUrl}
      isOpen={isOpen}
      modalWrapperTranslations={{
        title: 'Service image',
        subtitle: '200 x 200 px recommended Β· JPG, JPEG and PNG files only Β· Up to 5MB',
        saveButtonLabel: 'Apply',
        cancelButtonLabel: 'Cancel',
        closeModalButtonLabel: 'Close crop modal',
      }}
      onCancel={handleClose}
      onClose={handleClose}
      onSuccessfulUpload={handleSave}
    />
  </Box>
);

API Reference​

CropModal​

PropsTypeDescriptionDefault
isOpenbooleanControls whether the modal is visible._
imageUrlstring | nullInitial image URL. In online mode, a remote image can be fetched; otherwise users upload locally._
modalWrapperTranslationsModalWrapperTranslationsLabels for modal actions and headers. All fields are required._
modalWrapperTranslations.titlestringModal header title text.
modalWrapperTranslations.subtitlestringModal header subtitle/description text.
modalWrapperTranslations.saveButtonLabelstringText for the save/apply button.
modalWrapperTranslations.cancelButtonLabelstringText for the cancel button.
modalWrapperTranslations.closeModalButtonLabelstringAccessibility label for the close button (screen readers).
cropAreaTranslations?CropAreaTranslationsThese labels populate accessible names and tooltips for controls, and optionally render an "Upload new image" action on non-responsive layouts._
cropAreaTranslations?.zoomInButtonLabel?stringOptional label for the zoom-in button. Keep short and descriptive for screen readers (e.g., "Zoom in").
cropAreaTranslations?.zoomOutButtonLabel?stringOptional label for the zoom-out button. Keep short and descriptive for screen readers (e.g., "Zoom out").
cropAreaTranslations?.uploadNewButtonLabel?stringOptional label for the action to upload a new image. Rendered in the CropArea only when not in a responsive breakpoint.
dropAreaTranslations?DropAreaTranslationsLabels for file drop area. All fields are required._
dropAreaTranslations?.uploadImageButtonLabelstringText for the upload new file button.
dropAreaTranslations?.bottomTextSuffix?stringOptional text displayed after the link. Completes the inline sentence, e.g., " to continue."
dropAreaTranslations?.bottomTextPrefix?stringOptional text displayed before the link. Useful for sentences like "Drag and drop an image or ". When omitted, nothing is rendered.
dropAreaTranslations?.bottomLinkText?stringOptional text for the helper link (e.g., "browse files"). The link is rendered only when bottomLinkUrl is also provided.
dropAreaTranslations?.bottomLinkUrl?stringOptional absolute URL opened in a new tab when the helper link is clicked.
errorTranslations?ErrorTranslationsError messages for different scenarios. All fields are optional with default values.{fileSizeExceedsLimitError: 'This image file is too big. Please upload another.', fileTypeMismatchError: 'The file type is not supported.', fileUploadError: 'An error occurred while uploading the file.', androidFileReadError: 'Image couldn’t be uploaded. Please try again.', fetchIncomingImageError: 'An error occurred while fetching the image.', localFileProcessingError: 'An error occurred while uploading the file.' }
errorTranslations?.fileSizeExceedsLimitError?stringError when file size exceeds limit.This image file is too big. Please upload another.
errorTranslations?.fileTypeMismatchError?stringError when file type is not allowed.The file type is not supported.
errorTranslations?.fileUploadError?stringGeneric processing error message.An error occurred while uploading the file.
errorTranslations?.androidFileReadError?stringError during upload process.Image couldn’t be uploaded. Please try again.
errorTranslations?.fetchIncomingImageError?stringNetwork-related error message.An error occurred while fetching the image.
errorTranslations?.localFileProcessingError?stringError when local file processing fails.An error occurred while processing the image.
cropViewBoxOptions?CropViewBoxOptionsCrop box configuration: width, aspectRatio, and roundingStyle. All fields are optional with default values.{ width: 200, aspectRatio: 1, roundingStyle: "none" }
cropViewBoxOptions?.width?numberWidth of the crop box in pixels.200
cropViewBoxOptions?.aspectRatio?numberAspect ratio of the crop box (width/height).1
cropViewBoxOptions?.roundingStyle?"none" | "rounded" | "circle"Visual style of the crop box corners.none
generalOptions?GeneralOptionsCanvas sizing and behavior configuration. All fields are optional with default values.{ canvasWidth: 500, canvasHeight: 300, maxScaleFactor: 5, totalSidePadding: 48, saveButtonVariant: "accentPrimary" }
generalOptions?.canvasWidth?numberWidth of the canvas in pixels.500
generalOptions?.canvasHeight?numberHeight of the canvas in pixels.300
generalOptions?.maxScaleFactor?numberMaximum zoom scale factor for the image.5
generalOptions?.totalSidePadding?numberTotal padding on sides of the canvas in pixels.48
generalOptions?.saveButtonVariant?stringVisual variant for the save button.accentPrimary
imageProcessingOptions?ImageProcessingOptionsLocal file validation and optimization configuration. All fields are optional with default values.{ maxWidth: 2560, maxHeight: 2560, enableResize: true, maxFileSizeMB: 5, enableCompression: true, compressionQuality: 0.6, allowedMimeTypes: ["image/png", "image/jpeg", "image/jpg"] }
imageProcessingOptions.maxWidth?numberMaximum width for processed images in pixels.2560
imageProcessingOptions.maxHeight?numberMaximum height for processed images in pixels.2560
imageProcessingOptions.enableResize?booleanWhether to enable automatic image resizing.true
imageProcessingOptions.maxFileSizeMB?numberMaximum allowed file size in megabytes.5
imageProcessingOptions.enableCompression?booleanWhether to enable image compression.true
imageProcessingOptions.compressionQuality?numberCompression quality (0-1, where 1 is highest quality).0.6
imageProcessingOptions.allowedMimeTypes?string[]Array of allowed MIME types for uploaded files.["image/png", "image/jpeg", "image/jpg"]
apiOptions?ApiOptions | undefinedEnable online mode: provide uploadUrl, getAccessToken, and helpers to fetch/upload images. When omitted, the widget operates in offline mode using blob URLs only.
apiOptions?.uploadUrlstringURL endpoint for uploading processed images.
apiOptions?.getAccessToken() => Promise<string>Function that returns an access token for API authentication.
apiOptions?.sendFormDataRequest?(url: string, file: File, formParams: Record<string, string>) => Promise<string>Function to send form data (e.g., image file) to server and return image URL.
apiOptions?.fetchInitialImageUrl?(url: string) => Promise<Blob>;Function to fetch initial image URL from server.
apiOptions?.fetchGoogleStorageUploadUrl?(accessToken: string, fileName: string) => Promise<GoogleStorageApiResponse>Function to fetch Google Storage upload URL from server.
onSuccessfulUpload(url: string, imageData?: ImageData) => voidCalled after a successful image upload and client-side processing._
onClose() => voidCalled when the modal is closed via close button, overlay click, or pressing Esc (when enabled)._
onCancel() => voidCalled when the user clicks the Cancel action in the modal._
onError?(error: ErrorDetails) => voidOptional error callback for recoverable failures in the workflow._

Style API​

Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.

Please refer to the Style API documentation for more insights.

CropModal parts​

const [isOpen, setIsOpen] = React.useState(false);
const [imageUrl, setImageUrl] = React.useState(null);

const handleClose = React.useCallback(() => setIsOpen(false), []);
const handleSave = React.useCallback((newUrl) => {
  setIsOpen(false);
  setImageUrl(newUrl);
}, []);

return (
  <Box>
    <Avatar name="Default" size="96">
      {imageUrl ? <AvatarImage src={imageUrl} /> : null}
    </Avatar>
    <CropModal
      cropViewBoxOptions={{}}
      dropAreaTranslations={{
        uploadImageButtonLabel: 'Upload',
      }}
      imageUrl={imageUrl}
      isOpen={isOpen}
      modalWrapperTranslations={{
        title: 'Default story',
        subtitle: 'That`s what you get with the default/minimal configuration',
        saveButtonLabel: 'Save',
        cancelButtonLabel: 'Cancel',
        closeModalButtonLabel: 'Close crop modal',
      }}
      onCancel={handleClose}
      onClose={handleClose}
      onSuccessfulUpload={handleSave}
      className="border-4 border-secondary"
      classNames={{
        modalWrapper: 'z-[1202]',
        footer: 'bg-[#F7F9FA] p-5 rounded-b-12px',
        header: 'bg-[#FF0000] p-5 rounded-t-12px',
        saveButton: 'p-5 rounded-12px',
        subtitle: 'text-body-20',
        title: 'text-body-30 text-positive',
      }}
    />
    <Box className="flex mt-2 justify-center items-center gap-2">
      <Button onClick={() => setIsOpen(true)}>Open Crop Modal</Button>
      <Button isDisabled={!imageUrl} onClick={() => setImageUrl(null)}>
        Clear image
      </Button>
    </Box>
  </Box>
);
Stylable PartsDescription
rootThe container that wraps the entire CropModal component.
modalWrapperThe overlay and container of the modal; controls z-order and layout.
modalHeaderThe header area including the title and close button.
titleThe title text displayed in the modal header.
modalContentThe body area containing the drop area, loader, or crop area.
modalFooterThe footer container with Save and Cancel buttons.
saveButtonThe primary action button used to save changes.
cancelButtonThe secondary action button used to cancel and close.

Usage guidelines​

Do

  1. Initialize CropModal with proper imageProcessingOptions constraints:
    • maxWidth & maxHeight: Use 2560 for banners, 1024 for logos/service images (default: 2560)
    • maxFileSizeMB: Set appropriate file size limits (default: 5MB)
    • allowedMimeTypes: Restrict to expected formats (default: ['image/png', 'image/jpeg', 'image/jpg'])
    • enableResize & enableCompression: Control processing behavior (both default to true)
  2. Implement comprehensive error handling with the onError callback:
    const handleError = (errorDetails: ErrorDetails) => {
      console.error(`Error in ${errorDetails.location}:`, errorDetails.message);
      // Log to monitoring service
      analytics.track('crop_modal_error', {
        location: errorDetails.location,
        message: errorDetails.message,
        stack: errorDetails.stack
      });
      // Show user-friendly notification
      showNotification(errorDetails.message);
    };
    
  3. Configure custom error messages with errorTranslations:
    errorTranslations={{
      fileSizeExceedsLimitError: 'File too large. Please use an image under 5MB.',
      fileTypeMismatchError: 'Only JPG, JPEG, and PNG files are supported.',
      fileUploadError: 'Upload failed. Please check your connection and try again.',
      fetchIncomingImageError: 'Could not load the image. Please try again.',
      localFileProcessingError: 'Image processing failed. Please try a different file.',
      androidFileReadError: 'Could not read the file. Please try again.'
    }}
    
  4. Support both offline (local processing) and online (API upload) processing modes:
    • Offline mode: Omit apiOptions for local-only processing with blob output
    • Online mode: Provide apiOptions for direct cloud upload integration
  5. Persist crop box configurations (cropViewBoxOptions) for consistent user experience:
    • Save width, aspectRatio, and roundingStyle in user preferences
    • Restore previous crop settings when reopening the modal

Don’t

  1. Don't ignore the onError callback β€” always implement error handling to track issues and provide user feedback.
  2. Don't block the UI during image processing β€” the component handles loading states, but ensure your app remains responsive.
  3. Don't set compressionQuality below 0.5 for photos β€” image artifacts become noticeable and degrade user experience.
  4. Don't use oversized dimensions for small images:
    • Avoid maxWidth: 2560 for logos/service images β€” use 1024 instead
  5. Don't ignore error locations β€” use errorDetails.location to implement specific error handling strategies:
    • REMOTE_IMAGE_FETCHING: Network connectivity issues
    • PROCESS_LOCAL_FILE: File reading or format problems
    • RESIZE_IMAGE: Memory or processing constraints
    • VALIDATE_IMAGE_TYPE: Unsupported file formats
    • SAVE_CHANGES: Upload or API failures

Best practices​

Do

  1. Prefer compressionQuality around 0.6 for photos.
  2. For banner images: Use default maxWidth/maxHeight (2560) for high-quality large displays.
  3. For logos and service images: Set maxWidth: 1024, maxHeight: 1024 since smaller images are sufficient.
  4. Restrict allowedMimeTypes to expected formats (defaults: ['image/png', 'image/jpeg', 'image/jpg']).

Don’t

  1. Don't set compressionQuality below 0.5 for photos; visible artifacts increase quickly.
  2. Don't upsample small images by using very large maxWidth/maxHeight β€” it degrades quality.
  3. Don't use banner-sized dimensions (2560) for logos/service images β€” it wastes bandwidth and storage.
  4. Don't accept unsupported file types; validate with allowedMimeTypes before processing.
  5. Don't rely solely on server-side compression; keep client-side behavior consistent across flows.