Crop

5 parts

Headless crop selector rendered over a media element (<img>, <video>, <canvas>). It owns a single crop rectangle — controlled via v-model or uncontrolled via defaultValue — and drives moving, eight-handle resizing, aspect-ratio locking, a rule-of-thirds grid, and a draw-from-empty create gesture. The rect lives in NORMALIZED 0..1 fractions of the media by default (resolution-independent) or in media pixels via units: 'pixels'. Supply the media size with mediaWidth/mediaHeight for standalone use, or mount inside a CanvasStageRoot and the Root reads the stage's content size automatically (props still win when given). It provides CropContext to CropArea, CropHandle, CropGrid, and CropOverlay, and emits cropCommit when a gesture or keypress settles. Reach for it to let a user pick a sub-rect of an image or video (avatar crop, thumbnail framing, redaction region).

Demo

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  CropRoot,
  CropArea,
  CropGrid,
  CropHandle,
  CropOverlay,
} from '@robonen/primitives/canvas/crop';

<CropRoot>
  <CropArea />
  <CropGrid />
  <CropHandle />
  <CropOverlay />
</CropRoot>

API Reference

CropRoot

Root

Headless crop selector rendered **over** a media element (`<img>`, `<video>`, `<canvas>`). It owns a single crop rectangle — controlled via `v-model` or uncontrolled via `defaultValue` — and drives moving, eight-handle resizing, aspect-ratio locking, a rule-of-thirds grid, and a draw-from-empty create gesture. The rect lives in NORMALIZED `0..1` fractions of the media by default (resolution-independent) or in media pixels via `units: 'pixels'`. Supply the media size with `mediaWidth`/`mediaHeight` for standalone use, or mount inside a `CanvasStageRoot` and the Root reads the stage's content size automatically (props still win when given). It provides {@link CropContext} to `CropArea`, `CropHandle`, `CropGrid`, and `CropOverlay`, and emits `cropCommit` when a gesture or keypress settles. Reach for it to let a user pick a sub-rect of an image or video (avatar crop, thumbnail framing, redaction region).

PropTypeDescription
modelValue?CropRect | nullThe crop rectangle (two-way via v-model). null means "no selection yet" — the Root falls back to the full frame or stays empty depending on createOnEmpty.
defaultValue?CropRect | nullUncontrolled initial rect.
units?CropUnitsCoordinate space for the rect and the size props.
aspectRatio?number | nullLocked width / height of the crop box (visual ratio), or null to resize freely.
minWidth?numberMinimum crop width, in the chosen units.
minHeight?numberMinimum crop height, in the chosen units.
mediaWidth?numberMedia width in pixels. Read from a CanvasStage ancestor when omitted.
mediaHeight?numberMedia height in pixels. Read from a CanvasStage ancestor when omitted.
constrain?booleanKeep the rect within the media bounds.
grid?booleanRender the rule-of-thirds grid.
createOnEmpty?booleanPointerdown on empty media draws a new rect from zero.
keyboardStep?numberKeyboard nudge step, in normalized units (scaled to px when units: 'pixels').
keyboardLargeStep?numberLarge keyboard step (Shift+Arrow).
disabled?booleanDisable all interaction.
dir?CropDirectionWriting direction (inherited from ConfigProvider when omitted).
Emits
EventPayload
update:modelValue[value: CropRect | null]

CropArea

Area

The crop rectangle surface — the draggable, focusable body of the selection. It sizes and positions itself from the Root's rect (× media size in pixel units), moves the whole rect on pointer drag (constrained to the media bounds), and moves it with the arrow keys when focused. Carries `role="group"`, `tabindex 0`, and an overridable `aria-label`. Place `CropHandle`s and `CropGrid` inside it. Hidden (renders nothing) while the selection is empty.

PropTypeDescription
label?stringAccessible label for the crop region.

CropGrid

Grid

The rule-of-thirds overlay: two vertical and two horizontal guide lines drawn across the crop box at the ⅓ and ⅔ marks. Purely presentational (`aria-hidden`), it renders only when the Root's `grid` prop is enabled and a selection exists. Place it inside `CropArea`. Each line is exposed as a slot entry so the consumer can style or replace the lines; the default slot renders four absolutely-positioned `<span>`s.

PropTypeDescription
divisions?numberNumber of columns/rows the grid divides the box into.

CropHandle

Handle

One of the eight resize handles — four corners and four edge midpoints. Render eight of these inside `CropArea`, one per `position`. Each is a native `<button type="button">` with an `aria-label` ("Resize top-left", etc.) and keyboard edge-resize: arrow keys move that edge/corner with the opposite edge fixed (Shift = large step), honouring aspect-ratio, min size, and bounds. Dragging resizes the same way. Positioned at its anchor on the crop box edge.

PropTypeDescription
positionCropHandlePositionWhich of the eight handles this is.
label?stringAccessible label override (defaults to "Resize ").

CropOverlay

Overlay

The dimmed scrim over the media OUTSIDE the crop rectangle. Renders four absolutely-positioned rects (top / bottom / left / right of the selection) so the consumer can tint the excluded region with a single `background`. Presentational (`aria-hidden`, `pointer-events: none`). Place it as a sibling of `CropArea`, spanning the media surface. Hidden while the selection is empty. The four rects are exposed via the default slot for full styling control.

No props or events — renders its element and forwards attributes.