Hooks
Seven hooks from @zriz/react that read the cropper and build parts of your own.
Call a hook in any component inside Root. This label reads edited once the crop changes:
"use client";
import { useCropperState } from "@zriz/react";
export const Status = () => {
const pristine = useCropperState((s) => s.pristine);
return <span>{pristine ? "original" : "edited"}</span>;
};Outside Root, pass the cropper yourself. It goes last in useCropperState, useCropperValue and useCropperFrame, and in the options of useZoom. Without it, they throw the error listed on errors.
Call methods from your own buttons
Each Root runs one cropper. Pass a ref to actionsRef to reach it once Root has mounted. The ref stays set while a suspense boundary shows its fallback, and is null while <Activity> hides Root. When Root unmounts, the ref clears with the passive effects, so a parent's useLayoutEffect in that commit still reads the cropper. On a keyed remount, the old Root lets go after the new one takes the ref. A ref object or a stable callback with no cleanup keeps the new cropper. An inline callback gets null last, and a cleanup your callback returns runs last, so neither suits a keyed Root. Cropper lists its methods.
"use client";
import type { Cropper as CropperApi } from "@zriz/react";
import * as Cropper from "@zriz/react/cropper";
import { useRef } from "react";
export const Editor = ({ src }: { src: string }) => {
const cropper = useRef<CropperApi>(null);
const save = () => {
console.log(cropper.current?.getPixelCrop());
};
return (
<>
<Cropper.Root src={src} actionsRef={cropper}>
<Cropper.Viewport
style={{ maxWidth: 480, height: 360 }}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area" />
</Cropper.Viewport>
</Cropper.Root>
<button onClick={save}>Save</button>
</>
);
};The hooks
useCropper
useCropper(options?) creates the cropper in a component above Root. Use it when that component needs the cropper during render. Hand the cropper to Root and to any control:
"use client";
import { attachImage } from "@zriz/core/dom";
import { useCropper } from "@zriz/react";
import * as Cropper from "@zriz/react/cropper";
import { useEffect } from "react";
export const Editor = ({ src }: { src: string }) => {
const cropper = useCropper({ maxZoom: 8 });
useEffect(() => () => attachImage(cropper, null), [cropper]);
return (
<>
<button onClick={() => cropper.reset()}>Reset</button>
<Cropper.Root cropper={cropper} src={src}>
<Cropper.Viewport
style={{ maxWidth: 480, height: 360 }}
>
<Cropper.Image />
<Cropper.Mask />
<Cropper.Area aria-label="Crop area" />
</Cropper.Viewport>
</Cropper.Root>
</>
);
};It takes the createCropper options on cropper except clock, plus the controlled aspect, rotation and zoom, and cropper to reuse one you already made. disabled, maxZoom, minSize, value and the two callbacks follow later renders. The cropper reads defaultValue, image, focalLength and plugins once, when it is made.
When Root gets cropper, it ignores its own defaultValue, image, focalLength and plugins, because the cropper already exists. Give each option to useCropper or to Root, never to both.
Root frees the photo only for a cropper it made. Free a useCropper cropper's photo yourself when the component unmounts, with attachImage(cropper, null) from @zriz/core/dom, as the snippet does. Add @zriz/core as in exporting.
Root keeps its photo while <Activity> hides it. React runs no cleanup when it removes a tree that is hidden, so Root cannot free the photo then. To remove a hidden Root, make the cropper with useCropper above it and call attachImage(cropper, null) yourself.
Pass one cropper to one Root. shape, motion and recenterDelay belong to the cropper, so a second Root overwrites them, even with its defaults. Keep Instructions in the Root that holds Area. Your own parts and hooks can reach the cropper through CropperContext or the cropper argument.
useCropperState
useCropperState(selector, cropper?) returns the part of the snapshot you select and renders again when it changes. The snapshot changes when a state flips, never on every frame.
- ready boolean
- Is
trueonce the cropper knows the photo's size. Theimageoption makes ittruebefore the file loads, so wait foronLoadbefore you export - interacting ChangeReason | null
- Names the gesture in progress
- settling boolean
- Is
truewhile any animation runs: a thrown dial, the recentre, a rotate or flip - pristine boolean
- Is
truewhile the crop matches the reset value - disabled boolean
- Is
truewhile the cropper refuses gestures and most writes, see cropper - aspect Aspect
- Holds the current aspect
- quarterTurns 0 | 1 | 2 | 3
- Holds the current quarter turns
- mirrored boolean
- Is
truewhile the photo is flipped - committed CropValue
- Holds the last committed value
Return one field or a primitive. A selector that builds a new object or array on every call makes React render forever.
useCropperValue
useCropperValue(cropper?) returns the last committed value and renders once per commit. A new value prop and writes with emit: false change it without a commit, so the hook does not render for them. The first fit when a photo loads does not commit either, but the hook renders when a photo arrives or leaves, so it shows that fit. The fields are on value.
"use client";
import { useCropperValue } from "@zriz/react";
export const Angle = () => {
const value = useCropperValue();
return <p>{value.rotation.toFixed(1)}°</p>;
};useCropperFrame
useCropperFrame(listener, cropper?) calls the listener on every frame that moved, without a render. The frame holds time, value and state, the crop in image pixels, which is null before a photo loads. Cropper lists its fields. Write the live angle straight into the DOM:
"use client";
import { useCropperFrame } from "@zriz/react";
import { useRef } from "react";
export const LiveAngle = () => {
const label = useRef<HTMLOutputElement>(null);
useCropperFrame(({ value }) => {
const angle = value.rotation.toFixed(1);
if (label.current) label.current.textContent = `${angle}°`;
});
return <output ref={label} />;
};useZoom
useZoom(options?) connects a slider of your own to the zoom. Each drag counts as one zoom-control gesture on events. It takes two options, max and cropper. Zoom builds a range input with it. It returns:
- value number
- Holds the zoom rounded to 3 decimals. It updates every frame
- min number
- Is always 1, the largest crop
- max number
- Is the
maxoption, or the cropper'smaxZoom, 15 by default.maxsets the slider's range only. The cropper still stops atmaxZoom - dragging boolean
- Is
truewhile the cropper has azoom-controlgesture open, so everyZoomand everyuseZoomon one cropper reads the same. A press onZoomholds it until release. AuseZoomchange holds it until the last held pointer or key lifts, or, with nothing held, 150 ms after the last change - onValueChange (zoom) => void
- Starts the gesture on the first call and sets the zoom. With no pointer or key held, the gesture ends 150 ms after the last call
- onValueCommitted () => void
- Ends the gesture. Call it when the slider lets go. The hook also ends it when the last held pointer or key lifts anywhere in the window, when the window loses focus and on unmount
useCoverScale
useCoverScale({ image, crop, rotation }) returns the smallest scale that keeps a straightened photo free of empty corners, for a crop centred on it. It calls no React hooks, so it is safe in loops and conditions. Outside React, coverScale(image, crop, radians) from @zriz/core, installed as in exporting, does the same.
"use client";
import { useCoverScale } from "@zriz/react";
export const Scale = () => {
const scale = useCoverScale({
image: { width: 4032, height: 3024 },
crop: { width: 4032, height: 3024 },
rotation: 5,
});
return <p>{scale.toFixed(4)}</p>;
};A full-frame crop of a 4032 × 3024 photo, straightened 5°, needs 1.1124. The photo grows 11.2%.
useRender
useRender(options) renders an element the way every zriz part does. Use it to build a part of your own. This is the label from the top of the page as a part:
"use client";
import { useCropperState, useRender } from "@zriz/react";
import type { ClassName, RenderProp } from "@zriz/react";
type EditedState = {
pristine: boolean;
};
type EditedProps = {
render?: RenderProp<EditedState>;
className?: ClassName<EditedState>;
};
export const Edited = ({ render, className }: EditedProps) => {
const pristine = useCropperState((s) => s.pristine);
return useRender({
tag: "span",
state: { pristine },
render,
className,
props: { children: pristine ? "original" : "edited" },
});
};Your part follows the render and className rules on composition. It takes:
- tag keyof JSX.IntrinsicElements Required
- Sets the tag, for example
"span" - state S Required
- Goes to
renderandclassNamefunctions - props object Required
- Sets the element's props. Take
renderand your own props out first, as the parts do - ref Ref<Element>
- Attaches to the element
- render RenderProp<S>
- Replaces the element, as on every part
- className ClassName<S>
- Sets the class: a string, or a function of state