# react-avatar-editor
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)