#1297·cropperjs

Using cropper-selection to limit boundaries in Vue3+Vite will render the width and height set by cropper-selection ineffective

Author: xxwangkaiminCreated Apr 20, 2026Updated Apr 20, 2026

Cropper version

2.1.1

Link to minimal reproduction

https://stackblitz.com/edit/vitejs-vite-7p1pknsl?file=src%2FApp.vue

Steps to reproduce

xml
<script setup lang="ts">
import { ref } from "vue"
import "cropperjs"
import type { CropperCanvas, CropperImage, CropperSelection, CropperHandle, CropperGrid, CropperCrosshair, CropperShade } from 'cropperjs';
import type { Selection } from "@cropper/element-selection"

const fileInputRef = ref<HTMLInputElement | null>(null)
const cropperImageDom = new Image()
const ready = ref(false)
const cropperImageSrc = ref("")
const cropperCanvasRef = ref<InstanceType<typeof CropperCanvas>>()
const cropperImageRef = ref<InstanceType<typeof CropperImage>>()
const cropperSelectionRef = ref<InstanceType<typeof CropperSelection>>()
const handleSelectFile = () => {
  if (fileInputRef.value) {
    fileInputRef.value.value = ""
    fileInputRef.value.click()
  }
}

const handleFileChange = (e: Event) => {
  const target = e.target as HTMLInputElement
  const file: File = (target.files as FileList)[0]
  if (!file) {
    return
  }
  cropperImageSrc.value = URL.createObjectURL(file)
  cropperImageDom.src = URL.createObjectURL(file)
  ready.value = true
}

const  inSelection = (selection: Selection, maxSelection: Selection) => {
  return (
    selection.x >= maxSelection.x
    && selection.y >= maxSelection.y
    && (selection.x + selection.width) <= (maxSelection.x + maxSelection.width)
    && (selection.y + selection.height) <= (maxSelection.y + maxSelection.height)
  )
}

function onSelectionChange(event: CustomEvent) {
  // Prevent the default behavior of the selection change event
  // to avoid the cropper from applying the selection immediately.
  // We want to apply our own constraints instead.
  if (!cropperCanvasRef.value || !cropperImageRef.value || !cropperSelectionRef.value) {
    return;
  }

  const selection = event.detail as Selection
  const cropperCanvasRect = cropperCanvasRef.value.getBoundingClientRect();
  const cropperImageRect = cropperImageRef.value.getBoundingClientRect();
  const width = Math.min(selection.width, cropperImageRect.width);
  const height = Math.min(selection.height, cropperImageRect.height);
  const minX = cropperImageRect.x - cropperCanvasRect.x;
  const minY = cropperImageRect.y - cropperCanvasRect.y;
  const maxX = minX + cropperImageRect.width - width;
  const maxY = minY + cropperImageRect.height - height;
  const x = Math.max(minX, Math.min(selection.x, maxX));
  const y = Math.max(minY, Math.min(selection.y, maxY));
  cropperSelectionRef.value.x = x;
  cropperSelectionRef.value.y = y;
  cropperSelectionRef.value.width = width;
  cropperSelectionRef.value.height = height;
  cropperSelectionRef.value.$render();

  const maxSelection: Selection = {
    x: cropperImageRect.left - cropperCanvasRect.left,
    y: cropperImageRect.top - cropperCanvasRect.top,
    width: cropperImageRect.width,
    height: cropperImageRect.height
  }
  if (!inSelection(selection, maxSelection)) {
    event.preventDefault()
  }
}
</script>

<template>
  <input ref="fileInputRef" type="file" accept="image/*" style="display: none" @change="handleFileChange">
  <button @click="handleSelectFile">选择图片</button>
  <div class="cropper-container">
    <cropper-canvas v-if="ready" ref="cropperCanvasRef">
      <cropper-image
        ref="cropperImageRef"
        alt="Picture"
        :src="cropperImageSrc"
        rotatable
        scalable
        skewable
        translatable
      />
      <cropper-shade />
      <cropper-handle action="select" plain />
      <cropper-selection
        ref="cropperSelectionRef"
        initial-coverage="0.5"
        movable
        resizable
        outlined
        precise
        width="160"
        height="90"
        @change="onSelectionChange"
      >
        <cropper-grid role="grid" covered />
        <cropper-crosshair centered />
        <cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)" />
        <cropper-handle action="n-resize" />
        <cropper-handle action="e-resize" />
        <cropper-handle action="s-resize" />
        <cropper-handle action="w-resize" />
        <cropper-handle action="ne-resize" />
        <cropper-handle action="nw-resize" />
        <cropper-handle action="se-resize" />
        <cropper-handle action="sw-resize" />
      </cropper-selection>
    </cropper-canvas>
  </div>
</template>

<style lang="scss" scoped>
.cropper-container {
  border: 1px solid var(--vp-c-divider);
  border-radius: 0.375rem;
  margin: 1rem 0;
  padding: 1.25rem 1.5rem;

  :deep(cropper-canvas) {
    height: 400px;
  }
}
</style>

What is expected?

Cropper-selection is displayed according to the configured width and height

What is actually happening?

Cropper-selection only displays a small portion in the middle

System Info

bash
System:
    OS: Windows 10 10.0.19045
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
    Memory: 3.26 GB / 15.73 GB
  Binaries:
    Node: 22.15.0 - C:\development\nodejs\node.EXE
    npm: 10.9.2 - C:\development\nodejs\npm.CMD
    pnpm: 10.9.0 - C:\development\nodejs\pnpm.CMD
  Browsers:
    Chrome: 147.0.7727.102
    Edge: Chromium (140.0.3485.66)
    Internet Explorer: 11.0.19041.5794
  npmPackages:
    cropperjs: ^2.1.1 => 2.1.1

Any additional comments?

No response