All questions

Image Cropper

Premium

Image Cropper

Build an image cropper whose bright selection rectangle can be moved or resized without leaving the 320 × 220 preview. Pointer and keyboard users must get the same geometry and the same live readout.

Signature

Implement the React App component in App.tsx. It receives no props. Keep the crop in useState<Rect> and start at { x: 60, y: 40, w: 180, h: 120 }.

Behavior contract

  • The crop body's pointerdown starts a move. Measure deltas from both the pointer-down coordinate and a snapshot of the starting rect so the box never jumps.
  • A corner handle's pointerdown starts a resize and must not also start a move. The grabbed corner follows the pointer in stage coordinates; its diagonal opposite remains fixed.
  • Active pointermove and pointerup listeners live on window. Release removes the exact listener references created for that gesture.
  • Move results clamp to x: 0..(320 - w) and y: 0..(220 - h). Resize results stay inside the stage, never invert, and keep w >= 40 and h >= 40. Round stored values to whole pixels.
  • With the crop body focused, Arrow keys move it by one pixel; Shift+Arrow moves it by ten. With a handle focused, Left/Right moves that corner's horizontal edge and Up/Down moves its vertical edge by the same step. Keyboard resize preserves the opposite corner and uses the same clamp as pointer resize.
  • Prevent default only for handled Arrow keys. Updates at a bound may leave the rect unchanged.

Accessibility contract

  • Keep the crop focusable with role="group", aria-describedby="crop-instructions", and a synchronized label: Crop region, x {x}, y {y}, width {w}, height {h}.
  • Use four native button handles named NW resize handle, NE resize handle, SW resize handle, and SE resize handle. Their DOM order is NW, NE, SW, SE.
  • Keep the visible instructions connected by crop-instructions. Keep the readout as role="status" with aria-live="polite"; its exact text is x {x} · y {y} · w {w} · h {h}.
  • Focus indicators must remain visible for both the crop and every handle. The decorative photo is hidden from assistive technology.

Examples

  • Initial output: x 60 · y 40 · w 180 · h 120.
  • Moving right by 200 pixels stops at x = 140, because 320 - 180 = 140.
  • From the initial rect, pressing Shift+ArrowLeft on the NW handle produces { x: 50, y: 40, w: 190, h: 120 }; the opposite SE point (240, 160) stays fixed.
  • Repeated ArrowRight on the SE handle stops at w = 260, the stage's right edge.

Notes

  • stageRef.current.getBoundingClientRect() converts viewport pointer coordinates into stage coordinates for resize.
  • Keep one pure resize(rect, corner, px, py) helper for pointer and keyboard input. React state, inline styles, the accessible label, and the readout all derive from the same rect.
  • The starter is complete-looking: markup, focus affordances, persistent CSS, and read-only state are supplied. Replace the empty handlers and make the state writable.
WE’RE LISTENING

Help make UIReady better

Found a bug or have an idea? Tell us about it.

Up to 3 files · 5 MiB each · JPG, PNG, WebP, MP4, WebM, MP3, M4A or WAV