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
import { Cropper } from '@fibrous/semi-ui-solid';
Basic usage
Use src to set the cropped image; use shape to set the shape of the cropping box, which defaults to square.


Code
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Radio, RadioGroup, type CropperElement, type CropperProps, type RadioChangeEvent } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [shape, setShape] = createSignal<CropperProps['shape']>('rect');
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
const onShapeChange = (event: RadioChangeEvent) => {
setShape(event.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={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} shape={shape()} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}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
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<>
<Cropper aspectRatio={3 / 4} ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}Controlled rotation/zooming of images
Control image rotation and zoom through rotate and zoom, and get the latest zoom value through onZoomChange. rotate is one-way: the component never changes it. Wheel zooming calls onZoomChange; the parent must update zoom in that callback, otherwise the zoom is reverted to the controlled value.


Code
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
const actionStyle = {
'margin-top': '20px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
width: 'fit-content',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [rotate, setRotate] = createSignal(0);
const [zoom, setZoom] = createSignal(1);
const [cropperUrl, setCropperUrl] = createSignal('');
const onSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
};
const onZoomSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
};
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<div id="cropper-container">
<Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} />
<div style={actionStyle}>
<span id="cropper-rotate-zoom-rotate">旋转</span>
<Slider aria-labelledby="cropper-rotate-zoom-rotate" style={{ width: '500px' }} 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: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
</div>
<br />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</div>
);
}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.


Code
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
const [cropperUrl, setCropperUrl] = createSignal('');
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<>
<strong>showResizeBox = false,并修改边框颜色</strong>
<Cropper ref={(node) => (cropper = node)} src="/assets/other/image.png" style={containerStyle} cropperBoxStyle={{ 'outline-color': 'var(--semi-color-bg-0)' }} showResizeBox={false} />
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ height: '400px' }} />
</Show>
</>
);
}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.


Code
import { Show, createSignal } from 'solid-js';
import { Button, Cropper, Slider, type CropperElement } from '@fibrous/semi-ui-solid';
const containerStyle = {
width: '550px',
height: '300px',
margin: '20px',
};
const actionStyle = {
'margin-top': '20px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
width: 'fit-content',
};
export default function CropperDemo() {
let cropper: CropperElement | undefined;
let previewWrapper: HTMLDivElement | undefined;
const [rotate, setRotate] = createSignal(0);
const [zoom, setZoom] = createSignal(1);
const [cropperUrl, setCropperUrl] = createSignal('');
const onSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setRotate(value);
};
const onZoomSliderChange = (value?: number | number[]) => {
if (typeof value === 'number') setZoom(value);
};
const onButtonClick = () => {
const canvas = cropper?.getCropperCanvas();
if (canvas) setCropperUrl(canvas.toDataURL());
};
return (
<div>
<Cropper ref={(node) => (cropper = node)} rotate={rotate()} zoom={zoom()} onZoomChange={(value) => setZoom(value)} preview={() => previewWrapper ?? null} src="/assets/root-web-sites/abstract.jpg" style={containerStyle} />
<div style={actionStyle}>
<span id="cropper-preview-rotate">旋转</span>
<Slider aria-labelledby="cropper-preview-rotate" style={{ width: '500px' }} 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: '500px' }} value={zoom()} step={0.1} min={0.1} max={3} onChange={onZoomSliderChange} />
</div>
<br />
<div style={{ display: 'flex' }}>
<div style={{ width: '50%', 'flex-grow': '1' }}>
<strong>实时预览</strong>
<div ref={(node) => (previewWrapper = node)} style={{ height: '300px', 'margin-top': '8px' }} />
</div>
<div style={{ width: '50%', 'flex-grow': '1', 'padding-left': '10px' }}>
<Button onClick={onButtonClick}>裁切</Button>
<br />
<br />
<Show when={cropperUrl()}>
<img src={cropperUrl()} alt="裁切结果" style={{ width: '90%' }} />
</Show>
</div>
</div>
</div>
);
}API
Props
| Property | Description | Type | Default |
|---|---|---|---|
| aspectRatio | Fixed aspect ratio of the crop box. When it changes after loading, the box is laid out again at the new ratio. | number | - |
| cropperBoxCls | Class name of the crop box. | string | - |
| cropperBoxStyle | Style of the crop box, merged with the size and offset the component computes; the size and offset win. | JSX.CSSProperties | - |
| defaultAspectRatio | Initial aspect ratio of the crop box, which changes while dragging. | number | 1 |
| fill | Fill color of the parts of the result that are not the image. | string | "rgba(0, 0, 0, 0)" |
| imgProps | Attributes passed to the img element being cropped. src and the layout belong to the component; class is merged with its own class name, style sits under its layout (the size and offset win), and onLoad is called after the component has handled it. alt defaults to an empty string and crossorigin to anonymous; both can be overridden. | CropperImgProps | - |
| maxZoom | Maximum zoom factor. | number | 3 |
| minZoom | Minimum zoom factor. | number | 0.1 |
| onZoomChange | Called with the new zoom when the wheel changes it. | Type(zoom: number) => void | - |
| preview | Function returning the DOM element of the preview container; the crop result is mirrored into it in real time. It may return null while the container is not mounted yet. | Type() => HTMLElement | null | - |
| ref | The root div element, which carries the getCropperCanvas method. | TypeRef<CropperElement> | - |
| rotate | Image rotation angle. It is one-way: the component never changes it. | number | - |
| shape | Shape of the crop box. | 'rect' | 'round' | 'roundRect' | "rect" |
| showResizeBox | Whether to show the resize handles on the corners of the crop box. | boolean | true |
| src | Address of the image to crop. | string | - |
| zoom | Controlled zoom, applied on top of the initial scaling that fits the image into the cropper. | number | - |
| zoomStep | Zoom change per wheel step. | number | 0.1 |
Methods
| Method | Description | Type |
|---|---|---|
| getCropperCanvas | Draws the area inside the crop box, at the image's own resolution, onto a new canvas. | Type() => HTMLCanvasElement |
class, style, ref and native attributes and events (such as id, data-*, aria-* and onKeyDown) land on the root <div>; ref gets the root element itself, with the getCropperCanvas method attached to it.
Accessibility
- With
aria-labeloraria-labelledbyand noroleon the root, the root getsrole="group"; aroleyou pass wins - The crop box is a focusable named group (
role="group", with its name and role description from the locale, andaria-describedbypointing to the usage hint). It becomes focusable once the image has loaded. Theimgbeing cropped is decorative: itsaltdefaults to an empty string and can be overridden throughimgProps - 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
aspectRatiois set the ratio is kept (a horizontal change is converted to height). This works only whileshowResizeBoxis 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
| Sass variable | Value | Usage |
|---|---|---|
| $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 |