使用场景
Cropper 用于裁切图片,支持自定义裁切框样式,可通过拖动调整裁切框位置,被裁切图片位置;可缩放,旋转被裁切图片。
代码演示
如何引入
import { Cropper } from '@fibrous/semi-ui-react';
基本用法
通过 src 设置被裁切的图片; 可通过 shape 设置裁切框形状,默认为方形。

方向键移动裁剪框,Shift 加方向键调整大小

代码
import { useState, useRef, useCallback } from 'react';
import { Cropper, Button, RadioGroup, Radio, type CropperHandle, type CropperProps, type RadioChangeEvent } from '@fibrous/semi-ui-react';
const containerStyle = {
width: 550,
height: 300,
margin: 20,
};
export default function CropperDemo() {
const ref = useRef<CropperHandle>(null);
const [shape, setShape] = useState<CropperProps['shape']>('rect');
const [cropperUrl, setCropperUrl] = useState('');
const onButtonClick = useCallback(() => {
const canvas = ref.current?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
}, []);
const onShapeChange = useCallback((e: RadioChangeEvent) => {
setShape(e.target.value);
}, []);
return (
<>
<RadioGroup aria-label="裁切框形状" onChange={onShapeChange} value={shape}>
<Radio value="rect">rect</Radio>
<Radio value="round">round</Radio>
<Radio value="roundRect">roundRect</Radio>
</RadioGroup>
<Cropper ref={ref} src="/assets/other/image.png" style={containerStyle} shape={shape} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
{cropperUrl && <img src={cropperUrl} alt="裁切结果" style={{ height: 400 }} />}
</>
);
}自定义裁切框比例
可通过 defaultAspectRatio 初始的裁切框比例(默认为 1)。可通过 aspectRatio 设置固定的裁切框比例。
设置 defaultAspectRatio仅对初始的裁切框比例生效, 拖动时,裁切框比例会随着拖动而变化。
设置 aspectRatio 时,裁切框比例固定,拖动时将裁切框将以此比例变化。图片加载后改变 aspectRatio,裁切框会按新比例重排:保持高度,宽度随比例变化,超出容器时缩到容器内,中心尽量保持不动;aspectRatio 变为 undefined 时不重排。

方向键移动裁剪框,Shift 加方向键调整大小

代码
import { useState, useRef, useCallback } from 'react';
import { Cropper, Button, type CropperHandle } from '@fibrous/semi-ui-react';
const containerStyle = {
width: 550,
height: 300,
margin: 20,
};
export default function CropperDemo() {
const ref = useRef<CropperHandle>(null);
const [cropperUrl, setCropperUrl] = useState('');
const onButtonClick = useCallback(() => {
const canvas = ref.current?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
}, []);
return (
<>
<Cropper aspectRatio={3 / 4} ref={ref} src="/assets/other/image.png" style={containerStyle} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
{cropperUrl && <img src={cropperUrl} alt="裁切结果" style={{ height: 400 }} />}
</>
);
}受控旋转/缩放图片
通过 rotate 和 zoom 控制图片旋转和缩放, 可通过 onZoomChange 拿到最新的 zoom 值。

方向键移动裁剪框,Shift 加方向键调整大小

旋转
缩放
代码
import { useState, useRef, useCallback } from 'react';
import { Cropper, Button, Slider, type CropperHandle } from '@fibrous/semi-ui-react';
const containerStyle = {
width: 550,
height: 300,
margin: 20,
};
const actionStyle = {
marginTop: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 'fit-content',
};
export default function CropperDemo() {
const [rotate, setRotate] = useState(0);
const [zoom, setZoom] = useState(1);
const ref = useRef<CropperHandle>(null);
const [cropperUrl, setCropperUrl] = useState('');
const onZoomChange = useCallback((value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
}, []);
const onSliderChange = useCallback((value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
}, []);
const onButtonClick = useCallback(() => {
const canvas = ref.current?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
}, []);
return (
<div id="cropper-container">
<Cropper ref={ref} src="/assets/other/image.png" style={containerStyle} rotate={rotate} zoom={zoom} onZoomChange={onZoomChange} />
<div style={actionStyle}>
<span id="cropper-rotate-zoom-rotate">旋转</span>
<Slider aria-labelledby="cropper-rotate-zoom-rotate" style={{ width: 500 }} value={rotate} step={1} min={-360} max={360} onChange={onSliderChange} />
</div>
<div style={actionStyle}>
<span id="cropper-rotate-zoom-zoom">缩放</span>
<Slider aria-labelledby="cropper-rotate-zoom-zoom" style={{ width: 500 }} value={zoom} step={0.1} min={0.1} max={3} onChange={onZoomChange} />
</div>
<br />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
{cropperUrl && <img src={cropperUrl} alt="裁切结果" style={{ height: 400 }} />}
</div>
);
}裁切框设置
可通过 cropperBoxStyle, cropperBoxCls 自定义裁切框样式。可通过 showResizeBox 设置是否展示裁切框边角的调整块。

方向键移动裁剪框,Shift 加方向键调整大小

代码
import { useState, useRef, useCallback } from 'react';
import { Cropper, Button, type CropperHandle } from '@fibrous/semi-ui-react';
const containerStyle = {
width: 550,
height: 300,
margin: 20,
};
export default function CropperDemo() {
const ref = useRef<CropperHandle>(null);
const [cropperUrl, setCropperUrl] = useState('');
const onButtonClick = useCallback(() => {
const canvas = ref.current?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
}, []);
return (
<>
<strong>showResizeBox = false,并修改边框颜色</strong>
<Cropper ref={ref} src="/assets/other/image.png" style={containerStyle} cropperBoxStyle={{ outlineColor: 'var(--semi-color-bg-0)' }} showResizeBox={false} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
{cropperUrl && <img src={cropperUrl} alt="裁切结果" style={{ height: 400 }} />}
</>
);
}实时预览裁切效果
通过 preview 指定预览容器,实时预览裁切效果。

方向键移动裁剪框,Shift 加方向键调整大小

旋转
缩放
实时预览
代码
import { useState, useRef, useCallback } from 'react';
import { Cropper, Button, Slider, type CropperHandle } from '@fibrous/semi-ui-react';
const containerStyle = {
width: 550,
height: 300,
margin: 20,
};
const actionStyle = {
marginTop: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 'fit-content',
};
export default function CropperDemo() {
const [rotate, setRotate] = useState(0);
const [zoom, setZoom] = useState(1);
const [cropperUrl, setCropperUrl] = useState('');
const ref = useRef<CropperHandle>(null);
const onZoomChange = useCallback((value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
}, []);
const onSliderChange = useCallback((value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
}, []);
const onButtonClick = useCallback(() => {
const canvas = ref.current?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
}, []);
const preview = useCallback(() => {
const previewContainer = document.getElementById('previewWrapper') as HTMLElement;
return previewContainer;
}, []);
return (
<div>
<Cropper ref={ref} src="/assets/root-web-sites/abstract.jpg" style={containerStyle} rotate={rotate} zoom={zoom} onZoomChange={onZoomChange} preview={preview} />
<div style={actionStyle}>
<span id="cropper-preview-rotate">旋转</span>
<Slider aria-labelledby="cropper-preview-rotate" style={{ width: 500 }} value={rotate} step={1} min={-360} max={360} onChange={onSliderChange} />
</div>
<div style={actionStyle}>
<span id="cropper-preview-zoom">缩放</span>
<Slider aria-labelledby="cropper-preview-zoom" style={{ width: 500 }} value={zoom} step={0.1} min={0.1} max={3} onChange={onZoomChange} />
</div>
<br />
<div style={{ display: 'flex' }}>
<div style={{ width: '50%', flexGrow: 1 }}>
<strong>实时预览</strong>
<div id="previewWrapper" style={{ height: 300, marginTop: 8 }} />
</div>
<div style={{ width: '50%', flexGrow: 1, paddingLeft: 10 }}>
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
{cropperUrl && <img src={cropperUrl} alt="裁切结果" style={{ width: '90%' }} />}
</div>
</div>
</div>
);
}API
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| aspectRatio | 固定的裁切框宽高比,拖动时保持该比例;加载后改变时,裁切框按新比例重排。 | number | - |
| className | 根元素的类名。 | string | - |
| cropperBoxCls | 裁切框类名。 | string | - |
| cropperBoxStyle | 裁切框样式,与组件计算的宽高和位移合并,宽高与位移以组件为准。 | React.CSSProperties | - |
| defaultAspectRatio | 裁切框的初始宽高比,拖动时会随之变化。 | number | 1 |
| fill | 裁切结果中非图片部分的填充色。 | string | "rgba(0, 0, 0, 0)" |
| imgProps | 传给被裁切图片 img 标签的属性。src 由 src 属性决定;className 与组件自身的类名合并;style 位于组件布局之下,宽高与位移以组件为准;onLoad 在组件处理完成后调用;alt 默认为空字符串,crossOrigin 默认为 anonymous,均可覆盖。 | TypeReact.ImgHTMLAttributes<HTMLImageElement> | - |
| maxZoom | 最大缩放倍数。 | number | 3 |
| minZoom | 最小缩放倍数。 | number | 0.1 |
| onZoomChange | 滚轮缩放时触发,参数为新的缩放比例。 | Type(zoom: number) => void | - |
| preview | 返回预览容器 DOM 的函数,裁切效果会实时镜像到该容器;可返回 null,表示容器尚未挂载。 | Type() => HTMLElement | null | - |
| ref | 组件句柄,其上的 getCropperCanvas 方法用于获取裁切结果;它不是根元素。 | TypeReact.Ref<CropperHandle> | - |
| rotate | 图片旋转角度,单位为度。单向绑定,组件不会改变它。 | number | - |
| shape | 裁切框形状,可选 rect、round 或 roundRect。 | 'rect' | 'round' | 'roundRect' | "rect" |
| showResizeBox | 是否展示裁切框边角的调整块。 | boolean | true |
| src | 被裁切图片的地址。 | string | - |
| style | 根元素的样式。 | React.CSSProperties | - |
| zoom | 受控的缩放比例,叠加在图片适应裁切区域的初始缩放之上。 | number | - |
| zoomStep | 滚轮缩放的步长。 | number | 0.1 |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| getCropperCanvas | 将裁切框内的区域按图片自身的分辨率绘制到新的 canvas 上。 | Type() => HTMLCanvasElement |
未声明的原生属性(如 id、data-*、aria-*、onKeyDown)落在根 <div> 上;role 可被覆盖。ref 拿到的是组件句柄,不是根元素。
Accessibility
- 根元素传入
aria-label或aria-labelledby且未传role时,自动带role="group";传入的role优先 - 裁切框是可聚焦的命名分组(
role="group",名称与角色描述取自语言包,并通过aria-describedby指向操作提示),图片加载完成后才可聚焦。被裁切的img是装饰性的,alt默认为空字符串,可经imgProps覆盖 - 只有主按钮的鼠标按下,以及 touch、pen 触点,才开始拖动裁切框、调整块和图片;鼠标中键、右键按下不会开始拖动
键盘和焦点
- 裁切框是组件内唯一的 Tab 停靠点,图片加载完成后才可聚焦
- 焦点在裁切框上时,方向键每次移动裁切框 10px,不会移出容器
- Shift + 方向键每次移动裁切框右下角 10px 来调整大小,限制在容器内;设置了
aspectRatio时保持该比例(横向的变化换算为高度)。仅在showResizeBox为 true(有调整块)时可用,为 false 时 Shift + 方向键不起作用 - 缩放与旋转没有内置的键盘操作,需要使用方提供可用键盘操作的控件(如示例中的 Slider)
设计变量
下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $color-cropper_mask-bg | var(--semi-color-overlay-bg) | |
| $color-cropper_box-outline | var(--semi-color-primary) | |
| $color-cropper_box_corner-bg | var(--semi-color-primary) | |
| $width-cropper_box-outline | 1px | |
| $width-cropper_box_corner | 10px |