All questions

Image Cropper

Premium

Image Cropper

Build the crop tool you see on every avatar upload: a bright rectangle floating over a photo that you can drag to reposition and resize from any of its four corners, with a live x / y / w / h readout underneath. The whole thing is one rectangle of state, { x, y, w, h }, and the real work is splitting a pointer gesture into two jobs — move the box, or resize it — while keeping it clamped inside the image.

Task

The starter App.tsx renders the crop box in its resting position over a gradient photo. Make it interactive:

  1. Hold the rect. Track the crop with useState({ x: 60, y: 40, w: 180, h: 120 }) and drive the .crop inline left / top / width / height and the readout from it.
  2. Drag to move. onPointerDown on the crop body starts a move: remember where the pointer started and the rect started, add pointermove / pointerup listeners on window, and translate x / y by the pointer delta.
  3. Resize from a corner. onPointerDown on a handle starts a resize from that corner. The grabbed corner follows the pointer; the opposite corner stays fixed.
  4. Stay in bounds. Clamp the box inside 0..(320 - w) / 0..(220 - h), and never let it shrink below 40 × 40.

Examples

  • The page loads with a 180 × 120 crop at (60, 40) and the readout x 60 · y 40 · w 180 · h 120.
  • Pressing inside the box and dragging slides the whole rectangle; the numbers update live and the box stops at the photo edges.
  • Grabbing the bottom-right handle and dragging out grows width and height; grabbing the top-left handle drags the top-left corner while the bottom-right corner stays pinned.

Notes

  • One rect, many outputs. The box position, its size, and the readout all derive from the same { x, y, w, h } — don't track them separately.
  • Listen on window, not the handle. A drag has to keep tracking even when the pointer outruns the small box, so the move/up listeners live on window for the duration of the gesture.
  • Pointer Events, not mouse events. One pointerdown / pointermove / pointerup path covers mouse, touch, and pen; touch-action: none (already in the CSS) stops the browser from scrolling mid-drag.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium