mosch/react-avatar-editor

Small avatar & profile picture component. Resize and crop uploaded images using a intuitive user interface.

TypeScript

2,500

619 commits

updated Oct 1, 2026

See the code

README

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

npm i react-avatar-editor

Usage

Basic

import AvatarEditor from 'react-avatar-editor'

function MyEditor() {
  return (
    <AvatarEditor
      image="https://example.com/photo.jpg"
      width={250}
      height={250}
      border={50}
      color={[255, 255, 255, 0.6]}
      scale={1.2}
      rotate={0}
    />
  )
}

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.

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 (
    <div>
      <AvatarEditor
        ref={editor.ref}
        image="https://example.com/photo.jpg"
        width={250}
        height={250}
        border={50}
        scale={1.2}
      />
      <button onClick={handleSave}>Save</button>
    </div>
  )
}

Hook methods

MethodReturnsDescription
refRefObjectPass this to the ref prop of AvatarEditor.
getImage()HTMLCanvasElement | nullThe cropped image at the original resolution.
getImageScaledToCanvas()HTMLCanvasElement | nullThe cropped image scaled to the editor dimensions.
getCroppingRect()object | nullThe crop area as { x, y, width, height } (0–1 range).

With ref (alternative)

If you prefer using refs directly:

import { useRef } from 'react'
import AvatarEditor, { type AvatarEditorRef } from 'react-avatar-editor'

function MyEditor() {
  const editor = useRef<AvatarEditorRef>(null)

  return (
    <div>
      <AvatarEditor
        ref={editor}
        image="https://example.com/photo.jpg"
        width={250}
        height={250}
      />
      <button
        onClick={() => {
          const canvas = editor.current?.getImageScaledToCanvas()
        }}
      >
        Save
      </button>
    </div>
  )
}

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:

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.

<div style={{ width: '100%', maxWidth: 600 }}>
  <AvatarEditor image={image} width="100%" height={300} border={20} />
</div>

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:

import AvatarEditor from 'react-avatar-editor'
import Dropzone from 'react-dropzone'

function MyEditor() {
  const [image, setImage] = useState('https://example.com/photo.jpg')

  return (
    <Dropzone onDrop={([file]) => setImage(file)} noClick noKeyboard>
      {({ getRootProps, getInputProps }) => (
        <div {...getRootProps()}>
          <AvatarEditor width={250} height={250} image={image} />
          <input {...getInputProps()} />
        </div>
      )}
    </Dropzone>
  )
}

Animated rotation

The rotate prop can be animated using any animation library. Here's an example with motion:

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 (
    <>
      <AvatarEditor
        image="https://example.com/photo.jpg"
        rotate={animatedRotate}
      />
      <button onClick={() => setRotate((r) => r - 90)}>↺</button>
      <button onClick={() => setRotate((r) => r + 90)}>↻</button>
    </>
  )
}

Props

PropTypeDefaultDescription
imagestring | FileThe URL or File object of the image to edit.
widthnumber | string200Width of the crop area in pixels, or a percentage like "100%" (see Responsive size).
heightnumber | string200Height of the crop area in pixels, or a percentage like "50%".
bordernumber | number[]25Border size around the crop area. Use an array [horizontal, vertical] for different values.
borderRadiusnumber0Border radius of the crop area. Set to width / 2 for a circle.
colornumber[][0, 0, 0, 0.5]RGBA color of the crop mask overlay.
borderColornumber[]RGBA color of the 1px border around the crop area. No border if omitted.
backgroundColorstringBackground color for transparent images (CSS color string).
scalenumber1Zoom level. 1 = fit, > 1 = zoom in, < 1 = zoom out (requires disableBoundaryChecks).
rotatenumber0Rotation in degrees.
position{ x, y }Center of the crop area (0–1 range). Set this + onPositionChange for controlled panning.
styleCSSPropertiesAdditional CSS styles for the canvas element.
crossOriginstringcrossOrigin attribute for the image. Use "anonymous" for CORS images.
showGridbooleanfalseShow a rule-of-thirds grid overlay.
gridColorstring"#666"Color of the grid lines.
disableBoundaryChecksbooleanfalseAllow the image to be moved outside the crop boundary.
disableHiDPIScalingbooleanfalseDisable devicePixelRatio scaling. Can improve performance on mobile.
disableCanvasRotationbooleantrueWhen false, the canvas resizes to fit the rotated image.
onLoadStart() => voidCalled when image loading begins.
onLoadSuccess(image) => voidCalled when the image loads successfully.
onLoadFailure() => voidCalled when the image fails to load.
onImageReady() => voidCalled when the image is first painted on the canvas.
onImageChange() => voidCalled on every visual change (drag, scale, rotate, etc.).
onMouseUp() => voidCalled when the user releases the mouse after dragging.
onMouseMove(event) => voidCalled on every mouse/touch move while dragging.
onPositionChange(position) => voidCalled when the crop position changes. Receives { x, y }.
onRequestScaleChange(scale) => voidCalled when the user zooms via +/- keys, pinch, or wheel. Receives the requested new scale.
enableWheelZoombooleanfalseZoom with the mouse wheel / trackpad. Requires onRequestScaleChange.
keyboardStepnumber1Pixels to move per arrow key press. Shift multiplies by 10.

Contributing

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, mtlewis, jakerichan, hu9o, ggwzrd, nmn, kukagg, benwiley4000, ruipserra, rdw, sktt, RKJuve, tibotiber, aumayr, yamafaktory, codedmart, chengyin, MahdiHadrich, jyash97, fivenp, pvcresin, shakaman, oyeanuj, dev-nima, kpbp, DedaDev, vitbokisch, pekq, MateusZitelli, kimon89, xaviergonz, jbrumwell, luisrudge, bytor99999, Mitelak, sahanDissanayake, sle-c, YacheLee, yogthesharma, taro-shono, tanguyantoine, lulzsun, mloeks, metacortex, exiify, thinhvoxuan, tvankith, yarikgenza, zhipenglin, TheMcMurder, notjosh, xulww, jimniels, jeffkole, deadlyicon, velezjose, lixiaoyan, brigand, florapdx, kuhelbeher, chris-rudmin, bluej100, dehbmarques, kimorq

avatar
crop
image
javascript
profile-picture-component
react

mosch/react-avatar-editor

Small avatar & profile picture component. Resize and crop uploaded images using a intuitive user interface.

TypeScript

2,500

619 commits

updated Oct 1, 2026

See the code

README

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

npm i react-avatar-editor

Usage

Basic

import AvatarEditor from 'react-avatar-editor'

function MyEditor() {
  return (
    <AvatarEditor
      image="https://example.com/photo.jpg"
      width={250}
      height={250}
      border={50}
      color={[255, 255, 255, 0.6]}
      scale={1.2}
      rotate={0}
    />
  )
}

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.

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 (
    <div>
      <AvatarEditor
        ref={editor.ref}
        image="https://example.com/photo.jpg"
        width={250}
        height={250}
        border={50}
        scale={1.2}
      />
      <button onClick={handleSave}>Save</button>
    </div>
  )
}

Hook methods

MethodReturnsDescription
refRefObjectPass this to the ref prop of AvatarEditor.
getImage()HTMLCanvasElement | nullThe cropped image at the original resolution.
getImageScaledToCanvas()HTMLCanvasElement | nullThe cropped image scaled to the editor dimensions.
getCroppingRect()object | nullThe crop area as { x, y, width, height } (0–1 range).

With ref (alternative)

If you prefer using refs directly:

import { useRef } from 'react'
import AvatarEditor, { type AvatarEditorRef } from 'react-avatar-editor'

function MyEditor() {
  const editor = useRef<AvatarEditorRef>(null)

  return (
    <div>
      <AvatarEditor
        ref={editor}
        image="https://example.com/photo.jpg"
        width={250}
        height={250}
      />
      <button
        onClick={() => {
          const canvas = editor.current?.getImageScaledToCanvas()
        }}
      >
        Save
      </button>
    </div>
  )
}

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:

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.

<div style={{ width: '100%', maxWidth: 600 }}>
  <AvatarEditor image={image} width="100%" height={300} border={20} />
</div>

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:

import AvatarEditor from 'react-avatar-editor'
import Dropzone from 'react-dropzone'

function MyEditor() {
  const [image, setImage] = useState('https://example.com/photo.jpg')

  return (
    <Dropzone onDrop={([file]) => setImage(file)} noClick noKeyboard>
      {({ getRootProps, getInputProps }) => (
        <div {...getRootProps()}>
          <AvatarEditor width={250} height={250} image={image} />
          <input {...getInputProps()} />
        </div>
      )}
    </Dropzone>
  )
}

Animated rotation

The rotate prop can be animated using any animation library. Here's an example with motion:

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 (
    <>
      <AvatarEditor
        image="https://example.com/photo.jpg"
        rotate={animatedRotate}
      />
      <button onClick={() => setRotate((r) => r - 90)}>↺</button>
      <button onClick={() => setRotate((r) => r + 90)}>↻</button>
    </>
  )
}

Props

PropTypeDefaultDescription
imagestring | FileThe URL or File object of the image to edit.
widthnumber | string200Width of the crop area in pixels, or a percentage like "100%" (see Responsive size).
heightnumber | string200Height of the crop area in pixels, or a percentage like "50%".
bordernumber | number[]25Border size around the crop area. Use an array [horizontal, vertical] for different values.
borderRadiusnumber0Border radius of the crop area. Set to width / 2 for a circle.
colornumber[][0, 0, 0, 0.5]RGBA color of the crop mask overlay.
borderColornumber[]RGBA color of the 1px border around the crop area. No border if omitted.
backgroundColorstringBackground color for transparent images (CSS color string).
scalenumber1Zoom level. 1 = fit, > 1 = zoom in, < 1 = zoom out (requires disableBoundaryChecks).
rotatenumber0Rotation in degrees.
position{ x, y }Center of the crop area (0–1 range). Set this + onPositionChange for controlled panning.
styleCSSPropertiesAdditional CSS styles for the canvas element.
crossOriginstringcrossOrigin attribute for the image. Use "anonymous" for CORS images.
showGridbooleanfalseShow a rule-of-thirds grid overlay.
gridColorstring"#666"Color of the grid lines.
disableBoundaryChecksbooleanfalseAllow the image to be moved outside the crop boundary.
disableHiDPIScalingbooleanfalseDisable devicePixelRatio scaling. Can improve performance on mobile.
disableCanvasRotationbooleantrueWhen false, the canvas resizes to fit the rotated image.
onLoadStart() => voidCalled when image loading begins.
onLoadSuccess(image) => voidCalled when the image loads successfully.
onLoadFailure() => voidCalled when the image fails to load.
onImageReady() => voidCalled when the image is first painted on the canvas.
onImageChange() => voidCalled on every visual change (drag, scale, rotate, etc.).
onMouseUp() => voidCalled when the user releases the mouse after dragging.
onMouseMove(event) => voidCalled on every mouse/touch move while dragging.
onPositionChange(position) => voidCalled when the crop position changes. Receives { x, y }.
onRequestScaleChange(scale) => voidCalled when the user zooms via +/- keys, pinch, or wheel. Receives the requested new scale.
enableWheelZoombooleanfalseZoom with the mouse wheel / trackpad. Requires onRequestScaleChange.
keyboardStepnumber1Pixels to move per arrow key press. Shift multiplies by 10.

Contributing

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, mtlewis, jakerichan, hu9o, ggwzrd, nmn, kukagg, benwiley4000, ruipserra, rdw, sktt, RKJuve, tibotiber, aumayr, yamafaktory, codedmart, chengyin, MahdiHadrich, jyash97, fivenp, pvcresin, shakaman, oyeanuj, dev-nima, kpbp, DedaDev, vitbokisch, pekq, MateusZitelli, kimon89, xaviergonz, jbrumwell, luisrudge, bytor99999, Mitelak, sahanDissanayake, sle-c, YacheLee, yogthesharma, taro-shono, tanguyantoine, lulzsun, mloeks, metacortex, exiify, thinhvoxuan, tvankith, yarikgenza, zhipenglin, TheMcMurder, notjosh, xulww, jimniels, jeffkole, deadlyicon, velezjose, lixiaoyan, brigand, florapdx, kuhelbeher, chris-rudmin, bluej100, dehbmarques, kimorq

avatar
crop
image
javascript
profile-picture-component
react

Project health

Overall

4.2/10

Maintained

10/10

Code review

0/10
OpenSSF Scorecard

Latest release

16.0.0 · Oct 1, 2026

Releases this year

5

Significant stargazers

Espen Hovlandsdal

672 followers · starred Feb 2016

Kristian Hellang

905 followers · starred Nov 2015

Théo LUDWIG

125 followers · starred Feb 2024

Rafał Pitoń

279 followers · starred Feb 2022