Crop
5 partsHeadless 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
Anatomy
Import the parts and compose them. Each part forwards attributes to its underlying element.
import {
CropRoot,
CropArea,
CropGrid,
CropHandle,
CropOverlay,
} from '@robonen/primitives/canvas/crop';
<CropRoot>
<CropArea />
<CropGrid />
<CropHandle />
<CropOverlay />
</CropRoot>API Reference
CropRoot
RootHeadless 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).
| Prop | Type | Description |
|---|---|---|
modelValue? | CropRect | null | The 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 | null | Uncontrolled initial rect. |
units? | CropUnits | Coordinate space for the rect and the size props. |
aspectRatio? | number | null | Locked width / height of the crop box (visual ratio), or null to resize freely. |
minWidth? | number | Minimum crop width, in the chosen units. |
minHeight? | number | Minimum crop height, in the chosen units. |
mediaWidth? | number | Media width in pixels. Read from a CanvasStage ancestor when omitted. |
mediaHeight? | number | Media height in pixels. Read from a CanvasStage ancestor when omitted. |
constrain? | boolean | Keep the rect within the media bounds. |
grid? | boolean | Render the rule-of-thirds grid. |
createOnEmpty? | boolean | Pointerdown on empty media draws a new rect from zero. |
keyboardStep? | number | Keyboard nudge step, in normalized units (scaled to px when units: 'pixels'). |
keyboardLargeStep? | number | Large keyboard step (Shift+Arrow). |
disabled? | boolean | Disable all interaction. |
dir? | CropDirection | Writing direction (inherited from ConfigProvider when omitted). |
| Event | Payload |
|---|---|
update:modelValue | [value: CropRect | null] |
CropArea
AreaThe 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.
| Prop | Type | Description |
|---|---|---|
label? | string | Accessible label for the crop region. |
CropGrid
GridThe 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.
| Prop | Type | Description |
|---|---|---|
divisions? | number | Number of columns/rows the grid divides the box into. |
CropHandle
HandleOne 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.
| Prop | Type | Description |
|---|---|---|
position | CropHandlePosition | Which of the eight handles this is. |
label? | string | Accessible label override (defaults to "Resize |
CropOverlay
OverlayThe 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.