# react-avatar-editor npm version Downloads Avatar / profile picture cropping component for React. Resize, crop and rotate your uploaded image using a simple and clean user interface. ## Features - Fully typed, written in TypeScript - Works with React 17, 18, and 19 - Resize, crop, and rotate - Rounded or square crop area - Built-in loading indicator - `useAvatarEditor` hook for easy access to the editor API - Zero runtime dependencies ## Install ```sh npm i react-avatar-editor ``` ## Usage ### Basic ```tsx import AvatarEditor from 'react-avatar-editor' function MyEditor() { return ( ) } ``` ### With `useAvatarEditor` hook The `useAvatarEditor` hook provides a clean API to access the editor's methods without managing refs manually. All methods return `null` if the editor isn't ready or no image is loaded. ```tsx import AvatarEditor, { useAvatarEditor } from 'react-avatar-editor' function MyEditor() { const editor = useAvatarEditor() const handleSave = () => { const canvas = editor.getImageScaledToCanvas() if (canvas) { const dataUrl = canvas.toDataURL() // upload dataUrl to your server } } return (
) } ``` #### Hook methods | Method | Returns | Description | | -------------------------- | --------------------------- | ------------------------------------------------------- | | `ref` | `RefObject` | Pass this to the `ref` prop of `AvatarEditor`. | | `getImage()` | `HTMLCanvasElement \| null` | The cropped image at the original resolution. | | `getImageScaledToCanvas()` | `HTMLCanvasElement \| null` | The cropped image scaled to the editor dimensions. | | `getCroppingRect()` | `object \| null` | The crop area as `{ x, y, width, height }` (0–1 range). | ### With ref (alternative) If you prefer using refs directly: ```tsx import { useRef } from 'react' import AvatarEditor, { type AvatarEditorRef } from 'react-avatar-editor' function MyEditor() { const editor = useRef(null) return (
) } ``` ### Saving as JPEG (smaller files) `canvas.toDataURL()` and `canvas.toBlob()` produce a PNG by default. PNG is lossless, so a cropped photo can end up several times larger than the JPEG you uploaded. Pass a type and quality to get a JPEG instead: ```tsx const canvas = editor.getImageScaledToCanvas() canvas?.toBlob( (blob) => { if (!blob) return const formData = new FormData() formData.append('avatar', blob, 'avatar.jpg') fetch('/api/avatar', { method: 'POST', body: formData }) }, 'image/jpeg', 0.9, // quality, 0–1 ) ``` Also pick the right method for your use case: - `getImageScaledToCanvas()` returns the crop at the editor's `width` × `height`. This is usually what you want for avatars. - `getImage()` returns the crop at the **original image resolution**. With a large camera photo that can be thousands of pixels wide, so the file will be large even as a JPEG. JPEG has no transparency, so transparent areas (for example in an uploaded PNG) turn black. Set `backgroundColor` to fill them with a color, or keep PNG. ### Responsive size `width` and `height` also accept a percentage of the parent element. The editor measures its parent and follows it when it resizes. A percentage sets the size of the whole canvas, and the crop area is that minus `border` on each side. ```tsx
``` The canvas becomes `display: block` in this mode. A percentage `height` needs a parent with a fixed height, as in CSS. Resizing keeps the image, position and zoom. The crop area just changes size. ### With drag and drop Using [react-dropzone](https://github.com/react-dropzone/react-dropzone): ```tsx import AvatarEditor from 'react-avatar-editor' import Dropzone from 'react-dropzone' function MyEditor() { const [image, setImage] = useState('https://example.com/photo.jpg') return ( setImage(file)} noClick noKeyboard> {({ getRootProps, getInputProps }) => (
)}
) } ``` ### Animated rotation The `rotate` prop can be animated using any animation library. Here's an example with [motion](https://motion.dev): ```tsx import { useState } from 'react' import { useMotionValue, useSpring, useMotionValueEvent } from 'motion/react' import AvatarEditor from 'react-avatar-editor' function MyEditor() { const [rotate, setRotate] = useState(0) const [animatedRotate, setAnimatedRotate] = useState(0) const rotateMotion = useMotionValue(0) const rotateSpring = useSpring(rotateMotion, { stiffness: 200, damping: 25 }) useMotionValueEvent(rotateSpring, 'change', (v) => setAnimatedRotate(v)) if (rotateMotion.get() !== rotate) { rotateMotion.set(rotate) } return ( <> ) } ``` ## Props | Prop | Type | Default | Description | | ----------------------- | -------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `image` | `string \| File` | | The URL or File object of the image to edit. | | `width` | `number \| string` | `200` | Width of the crop area in pixels, or a percentage like `"100%"` (see [Responsive size](#responsive-size)). | | `height` | `number \| string` | `200` | Height of the crop area in pixels, or a percentage like `"50%"`. | | `border` | `number \| number[]` | `25` | Border size around the crop area. Use an array `[horizontal, vertical]` for different values. | | `borderRadius` | `number` | `0` | Border radius of the crop area. Set to `width / 2` for a circle. | | `color` | `number[]` | `[0, 0, 0, 0.5]` | RGBA color of the crop mask overlay. | | `borderColor` | `number[]` | | RGBA color of the 1px border around the crop area. No border if omitted. | | `backgroundColor` | `string` | | Background color for transparent images (CSS color string). | | `scale` | `number` | `1` | Zoom level. `1` = fit, `> 1` = zoom in, `< 1` = zoom out (requires `disableBoundaryChecks`). | | `rotate` | `number` | `0` | Rotation in degrees. | | `position` | `{ x, y }` | | Center of the crop area (0–1 range). Set this + `onPositionChange` for controlled panning. | | `style` | `CSSProperties` | | Additional CSS styles for the canvas element. | | `crossOrigin` | `string` | | `crossOrigin` attribute for the image. Use `"anonymous"` for CORS images. | | `showGrid` | `boolean` | `false` | Show a rule-of-thirds grid overlay. | | `gridColor` | `string` | `"#666"` | Color of the grid lines. | | `disableBoundaryChecks` | `boolean` | `false` | Allow the image to be moved outside the crop boundary. | | `disableHiDPIScaling` | `boolean` | `false` | Disable `devicePixelRatio` scaling. Can improve performance on mobile. | | `disableCanvasRotation` | `boolean` | `true` | When `false`, the canvas resizes to fit the rotated image. | | `onLoadStart` | `() => void` | | Called when image loading begins. | | `onLoadSuccess` | `(image) => void` | | Called when the image loads successfully. | | `onLoadFailure` | `() => void` | | Called when the image fails to load. | | `onImageReady` | `() => void` | | Called when the image is first painted on the canvas. | | `onImageChange` | `() => void` | | Called on every visual change (drag, scale, rotate, etc.). | | `onMouseUp` | `() => void` | | Called when the user releases the mouse after dragging. | | `onMouseMove` | `(event) => void` | | Called on every mouse/touch move while dragging. | | `onPositionChange` | `(position) => void` | | Called when the crop position changes. Receives `{ x, y }`. | | `onRequestScaleChange` | `(scale) => void` | | Called when the user zooms via +/- keys, pinch, or wheel. Receives the requested new scale. | | `enableWheelZoom` | `boolean` | `false` | Zoom with the mouse wheel / trackpad. Requires `onRequestScaleChange`. | | `keyboardStep` | `number` | `1` | Pixels to move per arrow key press. Shift multiplies by 10. | ## Contributing ```sh pnpm install # install dependencies pnpm build # build the library pnpm lint # run oxlint pnpm fmt # format with oxfmt pnpm demo:dev # run demo at localhost:3000 ``` ### Kudos Thanks to all contributors: [dan-lee](https://github.com/dan-lee), [mtlewis](https://github.com/mtlewis), [jakerichan](https://github.com/jakerichan), [hu9o](https://github.com/hu9o), [ggwzrd](https://github.com/ggwzrd), [nmn](https://github.com/nmn), [kukagg](https://github.com/kukagg), [benwiley4000](https://github.com/benwiley4000), [ruipserra](https://github.com/ruipserra), [rdw](https://github.com/rdw), [sktt](https://github.com/sktt), [RKJuve](https://github.com/RKJuve), [tibotiber](https://github.com/tibotiber), [aumayr](https://github.com/aumayr), [yamafaktory](https://github.com/yamafaktory), [codedmart](https://github.com/codedmart), [chengyin](https://github.com/chengyin), [MahdiHadrich](https://github.com/MahdiHadrich), [jyash97](https://github.com/jyash97), [fivenp](https://github.com/fivenp), [pvcresin](https://github.com/pvcresin), [shakaman](https://github.com/shakaman), [oyeanuj](https://github.com/oyeanuj), [dev-nima](https://github.com/dev-nima), [kpbp](https://github.com/kpbp), [DedaDev](https://github.com/DedaDev), [vitbokisch](https://github.com/vitbokisch), [pekq](https://github.com/pekq), [MateusZitelli](https://github.com/MateusZitelli), [kimon89](https://github.com/kimon89), [xaviergonz](https://github.com/xaviergonz), [jbrumwell](https://github.com/jbrumwell), [luisrudge](https://github.com/luisrudge), [bytor99999](https://github.com/bytor99999), [Mitelak](https://github.com/Mitelak), [sahanDissanayake](https://github.com/sahanDissanayake), [sle-c](https://github.com/sle-c), [YacheLee](https://github.com/YacheLee), [yogthesharma](https://github.com/yogthesharma), [taro-shono](https://github.com/taro-shono), [tanguyantoine](https://github.com/tanguyantoine), [lulzsun](https://github.com/lulzsun), [mloeks](https://github.com/mloeks), [metacortex](https://github.com/metacortex), [exiify](https://github.com/exiify), [thinhvoxuan](https://github.com/thinhvoxuan), [tvankith](https://github.com/tvankith), [yarikgenza](https://github.com/yarikgenza), [zhipenglin](https://github.com/zhipenglin), [TheMcMurder](https://github.com/TheMcMurder), [notjosh](https://github.com/notjosh), [xulww](https://github.com/xulww), [jimniels](https://github.com/jimniels), [jeffkole](https://github.com/jeffkole), [deadlyicon](https://github.com/deadlyicon), [velezjose](https://github.com/velezjose), [lixiaoyan](https://github.com/lixiaoyan), [brigand](https://github.com/brigand), [florapdx](https://github.com/florapdx), [kuhelbeher](https://github.com/kuhelbeher), [chris-rudmin](https://github.com/chris-rudmin), [bluej100](https://github.com/bluej100), [dehbmarques](https://github.com/dehbmarques), [kimorq](https://github.com/kimorq)