Why a Circle Crop Is Really a Square: Building an Image Cropper

You need a round photo of yourself for a conference slide. You crop it into a circle on your phone, paste it onto the slide, and get a circle sitting inside a white square. That white square is the whole story of this article. When I built ToolOnTap’s crop tool, the circle option was the part that needed the most thought, because no image file is round.

Three shapes, one crop box

The tool has three modes: Free, Square and Circle. Under the hood they’re the same crop box with different rules.

Mode Crop box Saved as
Free any shape the original format (JPG, PNG or WebP)
Square locked to 1:1 the original format
Circle locked to 1:1, drawn round on screen always PNG

The crop box itself comes from cropperjs, the 1.x version. It handles the parts that are tedious to get right by hand: dragging the box, dragging its corners, keeping it inside the image, and mapping screen pixels back to image pixels. It’s a heavy library compared to the rest of the page, so it only loads with the tools that need it, and the build turns off prefetching for it so other pages never download it.

These are the options I pass in:

cropper = new Cropper(img.value!, {
  aspectRatio: props.aspectRatio ?? Number.NaN,
  viewMode: 1,             // keep the crop box inside the image
  autoCropArea: 0.9,
  background: false,
  zoomOnWheel: false,      // don't hijack page scrolling
  checkOrientation: false, // browsers already apply EXIF orientation
  responsive: true,
})

Two of them came from mistakes. With zoomOnWheel left on, scrolling down the page over the image zoomed the photo instead of moving the page, which is confusing on a page where the help text sits below the tool. And checkOrientation: false matters because phone photos are often stored sideways with an EXIF tag that says “rotate me”. Current browsers already apply that tag when they draw the image. If cropperjs applied it a second time, the photo would end up turned twice.

Number.NaN is how cropperjs spells “no fixed ratio”, so Free mode passes it, and Square and Circle pass 1. When you switch modes, the tool calls setAspectRatio on the same cropper instead of building a new one.

Why the circle is a square

Every common image format (JPG, PNG, WebP) stores a rectangle of pixels. There’s no such thing as a round JPG. What looks like a round image is a square image whose corners are either transparent or hidden by the page that shows it.

So Circle mode does two separate things:

  1. On screen, it rounds the crop box with CSS (border-radius: 50% on the box), so you can see what will be kept. That’s only a preview.
  2. On export, it draws the square crop onto a new canvas through a circular clipping path. Everything outside the circle is left untouched, and on a fresh canvas untouched means fully transparent.

Here’s the export step:

// Largest centered circle, outside made transparent
function toCircle(src: HTMLCanvasElement) {
  const size = Math.min(src.width, src.height)
  const { canvas, ctx } = createCanvas(size, size)
  ctx.beginPath()
  ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
  ctx.clip()
  ctx.drawImage(src, (src.width - size) / 2, (src.height - size) / 2, size, size, 0, 0, size, size)
  return canvas
}

ctx.clip() turns the circle into a stencil: the next drawImage only paints inside it. The Math.min is a safety net. The crop box is already locked to 1:1, but if rounding left the canvas a pixel wider than it is tall, the circle still fits and stays centered.

The file that comes out is a square PNG. Its four corners are transparent, so on a slide, a website or a dark background you only see the circle. The result preview in the tool sits on a checkerboard so you can see where the transparency is.

Why circle crops are always PNG

JPG has no transparency. If you save those transparent corners as JPG, the encoder has to put some color there, and the browser’s canvas uses black. That’s why Circle mode ignores the original format and always saves PNG, which keeps an alpha channel.

The other modes keep the original format: JPG stays JPG, PNG stays PNG, and WebP stays WebP. Any other input, such as a GIF or a BMP, is saved as JPG. For JPG output the tool paints a white background first, so a PNG with transparent areas that ends up as JPG gets white instead of black behind it. JPGs are saved at 92% quality. The pixels you keep aren’t resized, so the crop has the same detail as the original in that area.

The file name tells you what happened: IMG_2041.jpg becomes IMG_2041-cropped.jpg, and a circle crop of it becomes IMG_2041-cropped.png.

Square or circle for a profile picture

This is the part people get wrong most often, and it’s the tip I put on the tool page: use Square for profile pictures on WhatsApp, Slack, LinkedIn and most other sites.

Those sites store a square image and draw the circle themselves when they display it. If you upload a circle PNG, the site puts its own circle mask on top of yours. If the two circles don’t line up exactly, you can get a thin ring of background color around your face, and a site that converts uploads to JPG has to fill the transparent corners with some color. A square photo avoids both.

Circle mode is for places that show your image exactly as it is:

  • a speaker photo on a slide
  • a team page on a website where you don’t control the CSS
  • a round logo or badge you’ll place on top of other things

Big photos and phones

A photo from a recent phone can be 12 megapixels or more, and the crop is drawn on a canvas at full resolution. iOS Safari has a hard limit on canvas size: above 4096 × 4096 pixels (16,777,216 pixels in total), drawing silently fails or the tab crashes. Before the canvas is created, the crop size goes through clampPixels, which scales it down, keeping the shape, only when it’s above that limit. Most crops of phone photos are well under it, so they keep every pixel.

Files are checked before anything is drawn: images over 50 MB are refused with a message, and HEIC photos (which only Safari can decode) are pointed to the HEIC converter instead of failing halfway through.

Cropping with a keyboard

cropperjs is built for a mouse or a finger. I added keyboard control on top, because a crop tool you can’t use without a pointer fails people who navigate with a keyboard. Focus the image, then:

  • Arrow keys move the box by 10 pixels.
  • Shift + arrows resize it.
  • Alt with either gives 1-pixel steps.

The handler reads the crop box position from cropperjs, adds the step and sets it back. It doesn’t need anything more clever than that, because cropperjs already keeps the box inside the image and enforces the 1:1 ratio in Square and Circle modes.

Try it

The crop tool runs in your browser, so your photo isn’t uploaded anywhere. Pick Square for avatars, Circle when you need the transparent corners, and rotate first if the photo is sideways, so the box lines up with the straightened picture. For more image tools, see all image tools.

Tools in this article

  • Crop an image

    Crop photos in your browser: free-form, square for profile pictures, or a circle with a transparent background. No upload, no watermark.

More articles