Show · Cropper

Cropper

Freely crop pictures

When to use

Cropper is used to crop pictures. It supports custom cropping box styles. The positions of the cropping box, cropped image can be adjusted by dragging. It can zoom and rotate the cropped pictures.

Demos

How to import

<script setup lang="ts">
    import { Cropper } from '@fibrous/semi-ui-vue';
</script>

Basic usage

Use src to set the cropped image; use shape to set the shape of the cropping box, which defaults to square.



Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Button, Cropper, Radio, RadioGroup, type CropperHandle, type CropperShape, type RadioValue } from '@fibrous/semi-ui-vue';

    const containerStyle = {
        width: '550px',
        height: '300px',
        margin: '20px',
    };

    const cropper = useTemplateRef<CropperHandle>('cropper');
    const shape = ref<RadioValue | undefined>('rect');
    const cropperUrl = ref('');

    const onButtonClick = () => {
        const canvas = cropper.value?.getCropperCanvas();
        if (canvas) cropperUrl.value = canvas.toDataURL();
    };
</script>

<template>
    <RadioGroup v-model="shape" aria-label="裁切框形状">
        <Radio value="rect">rect</Radio>
        <Radio value="round">round</Radio>
        <Radio value="roundRect">roundRect</Radio>
    </RadioGroup>
    <Cropper ref="cropper" :shape="shape as CropperShape" src="/assets/other/image.png" :style="containerStyle" />
    <Button @click="onButtonClick">裁切</Button>
    <br />
    <br />
    <img v-if="cropperUrl" alt="裁切结果" :src="cropperUrl" :style="{ height: '400px' }" />
</template>

Customize crop box ratio

The initial crop box ratio can be passed through defaultAspectRatio (default is 1). A fixed crop box ratio can be set via aspectRatio.

Setting defaultAspectRatio only takes effect on the initial crop box ratio. When dragging, the crop box ratio will change with dragging.

When setting aspectRatio, the crop box ratio is fixed, and the crop box will change according to this ratio when dragging. When aspectRatio changes after the image has loaded, the crop box is laid out again at the new ratio: its height is kept, its width follows the ratio, it shrinks to fit the container when it would overflow, and its center stays where it is as far as possible. When aspectRatio becomes undefined the box is not laid out again.



Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Button, Cropper, type CropperHandle } from '@fibrous/semi-ui-vue';

    const containerStyle = {
        width: '550px',
        height: '300px',
        margin: '20px',
    };

    const cropper = useTemplateRef<CropperHandle>('cropper');
    const cropperUrl = ref('');

    const onButtonClick = () => {
        const canvas = cropper.value?.getCropperCanvas();
        if (canvas) cropperUrl.value = canvas.toDataURL();
    };
</script>

<template>
    <Cropper ref="cropper" :aspectRatio="3 / 4" src="/assets/other/image.png" :style="containerStyle" />
    <Button @click="onButtonClick">裁切</Button>
    <br />
    <br />
    <img v-if="cropperUrl" alt="裁切结果" :src="cropperUrl" :style="{ height: '400px' }" />
</template>

Controlled rotation/zooming of images

Control image rotation and zoom through rotate and zoom. zoom can be bound two-way with v-model:zoom, or one-way with :zoom and @zoomChange; wheel zooming emits both update:zoom and zoomChange. rotate is one-way only: the component never changes it.

旋转
-360360
缩放
0.13



Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Button, Cropper, Slider, type CropperHandle } from '@fibrous/semi-ui-vue';

    const containerStyle = {
        width: '550px',
        height: '300px',
        margin: '20px',
    };

    const actionStyle = {
        marginTop: '20px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: 'fit-content',
    };

    const rotate = ref(0);
    const zoom = ref(1);
    const cropper = useTemplateRef<CropperHandle>('cropper');
    const cropperUrl = ref('');

    const onSliderChange = (value: number | number[]) => {
        if (typeof value === 'number') rotate.value = value;
    };

    const onZoomSliderChange = (value: number | number[]) => {
        if (typeof value === 'number') zoom.value = value;
    };

    const onButtonClick = () => {
        const canvas = cropper.value?.getCropperCanvas();
        if (canvas) cropperUrl.value = canvas.toDataURL();
    };
</script>

<template>
    <div id="cropper-container">
        <Cropper ref="cropper" v-model:zoom="zoom" :rotate="rotate" src="/assets/other/image.png" :style="containerStyle" />
        <div :style="actionStyle">
            <span id="cropper-rotate-zoom-rotate">旋转</span>
            <Slider aria-labelledby="cropper-rotate-zoom-rotate" :max="360" :min="-360" :modelValue="rotate" :step="1" :style="{ width: '500px' }" @update:modelValue="onSliderChange" />
        </div>
        <div :style="actionStyle">
            <span id="cropper-rotate-zoom-zoom">缩放</span>
            <Slider aria-labelledby="cropper-rotate-zoom-zoom" :max="3" :min="0.1" :modelValue="zoom" :step="0.1" :style="{ width: '500px' }" @update:modelValue="onZoomSliderChange" />
        </div>
        <br />
        <Button @click="onButtonClick">裁切</Button>
        <br />
        <br />
        <img v-if="cropperUrl" alt="裁切结果" :src="cropperUrl" :style="{ height: '400px' }" />
    </div>
</template>

Crop box settings

The crop box style can be customized through cropperBoxStyle, cropperBoxCls. You can use showResizeBox to set whether to display the adjustment blocks at the corners of the crop box.

showResizeBox = false,并修改边框颜色


Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Button, Cropper, type CropperHandle } from '@fibrous/semi-ui-vue';

    const containerStyle = {
        width: '550px',
        height: '300px',
        margin: '20px',
    };

    const cropper = useTemplateRef<CropperHandle>('cropper');
    const cropperUrl = ref('');

    const onButtonClick = () => {
        const canvas = cropper.value?.getCropperCanvas();
        if (canvas) cropperUrl.value = canvas.toDataURL();
    };
</script>

<template>
    <strong>showResizeBox = false,并修改边框颜色</strong>
    <Cropper ref="cropper" :cropperBoxStyle="{ outlineColor: 'var(--semi-color-bg-0)' }" :showResizeBox="false" src="/assets/other/image.png" :style="containerStyle" />
    <Button @click="onButtonClick">裁切</Button>
    <br />
    <br />
    <img v-if="cropperUrl" alt="裁切结果" :src="cropperUrl" :style="{ height: '400px' }" />
</template>

Live preview

Use preview to specify a container that previews the crop result in real time. preview is a function returning the container DOM; it may return null while the container is not mounted yet.

旋转
-360360
缩放
0.13

实时预览


Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Button, Cropper, Slider, type CropperHandle } from '@fibrous/semi-ui-vue';

    const containerStyle = {
        width: '550px',
        height: '300px',
        margin: '20px',
    };

    const actionStyle = {
        marginTop: '20px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: 'fit-content',
    };

    const rotate = ref(0);
    const zoom = ref(1);
    const cropperUrl = ref('');
    const cropper = useTemplateRef<CropperHandle>('cropper');
    const previewWrapper = useTemplateRef<HTMLDivElement>('previewWrapper');

    const onSliderChange = (value: number | number[]) => {
        if (typeof value === 'number') rotate.value = value;
    };

    const onZoomSliderChange = (value: number | number[]) => {
        if (typeof value === 'number') zoom.value = value;
    };

    const onButtonClick = () => {
        const canvas = cropper.value?.getCropperCanvas();
        if (canvas) cropperUrl.value = canvas.toDataURL();
    };

    // a function so the cropper reads the wrapper when it needs it; the ref is null until the wrapper has mounted
    const preview = () => previewWrapper.value;
</script>

<template>
    <div>
        <Cropper ref="cropper" v-model:zoom="zoom" :preview="preview" :rotate="rotate" src="/assets/root-web-sites/abstract.jpg" :style="containerStyle" />
        <div :style="actionStyle">
            <span id="cropper-preview-rotate">旋转</span>
            <Slider aria-labelledby="cropper-preview-rotate" :max="360" :min="-360" :modelValue="rotate" :step="1" :style="{ width: '500px' }" @update:modelValue="onSliderChange" />
        </div>
        <div :style="actionStyle">
            <span id="cropper-preview-zoom">缩放</span>
            <Slider aria-labelledby="cropper-preview-zoom" :max="3" :min="0.1" :modelValue="zoom" :step="0.1" :style="{ width: '500px' }" @update:modelValue="onZoomSliderChange" />
        </div>
        <br />
        <div :style="{ display: 'flex' }">
            <div :style="{ width: '50%', flexGrow: 1 }">
                <strong>实时预览</strong>
                <div ref="previewWrapper" :style="{ height: '300px', marginTop: '8px' }" />
            </div>
            <div :style="{ width: '50%', flexGrow: 1, paddingLeft: '10px' }">
                <Button @click="onButtonClick">裁切</Button>
                <br />
                <br />
                <img v-if="cropperUrl" alt="裁切结果" :src="cropperUrl" :style="{ width: '90%' }" />
            </div>
        </div>
    </div>
</template>

API

Props

PropertyDescriptionTypeDefault
srcAddress of the image to crop.stringsemiGlobalDefault("Cropper", "src")
imgPropsAttributes passed to the img element being cropped. src and the layout style belong to the component; class and style are merged with its own, and onLoad is called after the component has handled it. alt defaults to an empty string and crossorigin to anonymous; both can be overridden.ImgHTMLAttributessemiGlobalDefault("Cropper", "imgProps")
shapeShape of the crop box.CropperShapesemiDefault("Cropper", "shape", "rect")
aspectRatioFixed aspect ratio of the crop box. When it changes after loading, the box is laid out again at the new ratio.numbersemiGlobalDefault("Cropper", "aspectRatio")
defaultAspectRatioInitial aspect ratio of the crop box, which changes while dragging.numbersemiDefault("Cropper", "defaultAspectRatio", 1)
zoomControlled zoom, applied on top of the initial scaling that fits the image into the cropper.numbersemiGlobalDefault("Cropper", "zoom")
rotateImage rotation angle. It is one-way: the component never changes it.numbersemiGlobalDefault("Cropper", "rotate")
showResizeBoxWhether to show the resize handles on the corners of the crop box.booleansemiDefault("Cropper", "showResizeBox", true)
cropperBoxStyleStyle of the crop box.StyleValuesemiGlobalDefault("Cropper", "cropperBoxStyle")
cropperBoxClsClass name of the crop box.stringsemiGlobalDefault("Cropper", "cropperBoxCls")
fillFill color of the parts of the result that are not the image.stringsemiDefault("Cropper", "fill", "rgba(0, 0, 0, 0)")
maxZoomMaximum zoom factor.numbersemiDefault("Cropper", "maxZoom", 3)
minZoomMinimum zoom factor.numbersemiDefault("Cropper", "minZoom", 0.1)
zoomStepZoom change per wheel step.numbersemiDefault("Cropper", "zoomStep", 0.1)
previewFunction returning the DOM element that mirrors the cropped area live. It may return null while the element is not mounted yet.Type(() => HTMLElement | null | undefined)undefined

Events

EventDescriptionCallback parameters
update:zoomEmitted by wheel zooming, for v-model:zoom.[zoom: number]
zoomChangeEmitted by wheel zooming, with the same payload as update:zoom.[zoom: number]

Slots

SlotDescriptionScope parameters
None

Exposes

NameDescriptionType
getCropperCanvasDraws the area inside the crop box at the image's own resolution onto a new canvas and returns it. It throws when called before the image has loaded.Type() => HTMLCanvasElement

class, style, data-*, aria-* and native listeners (click, keydown and so on) land on the root element through attrs. The component ref exposes only getCropperCanvas, not the root DOM. When a global default is set for zoom or rotate, every Cropper becomes controlled.

Accessibility

  • With aria-label or aria-labelledby and no role on the root, the root gets role="group"; a role you pass wins
  • The crop box is a focusable named group (role="group", with its name and role description from the locale, and aria-describedby pointing to the usage hint). It becomes focusable once the image has loaded. The img being cropped is decorative: its alt defaults to an empty string and can be overridden through imgProps
  • Only a mouse press with the primary button, or a touch or pen contact, starts dragging the crop box, a resize handle or the image; a middle or right mouse press does not

Keyboard and focus

  • The crop box is the only tab stop in the component, and is focusable only after the image has loaded
  • With focus on the crop box, each arrow key moves the box by 10px, and the box stays inside the container
  • Shift + arrow keys move the bottom right corner of the box by 10px to resize it, within the container; when aspectRatio is set the ratio is kept (a horizontal change is converted to height). This works only while showResizeBox is true (the handles are shown); with false, Shift + arrow keys do nothing
  • Zoom and rotation have no built-in keyboard control; provide a keyboard-operable control for them (such as the Slider in the demos)

Design Token

These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
Sass variableValueUsage
$color-cropper_mask-bgvar(--semi-color-overlay-bg)
$color-cropper_box-outlinevar(--semi-color-primary)
$color-cropper_box_corner-bgvar(--semi-color-primary)
$width-cropper_box-outline1px
$width-cropper_box_corner10px