Skip to navigation

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.

Field
ready boolean
Is true once the cropper knows the photo's size. The image option makes it true before the file loads, so wait for onLoad before you export
interacting ChangeReason | null
Names the gesture in progress
settling boolean
Is true while any animation runs: a thrown dial, the recentre, a rotate or flip
pristine boolean
Is true while the crop matches the reset value
disabled boolean
Is true while 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 true while 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:

Field
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 max option, or the cropper's maxZoom, 15 by default. max sets the slider's range only. The cropper still stops at maxZoom
dragging boolean
Is true while the cropper has a zoom-control gesture open, so every Zoom and every useZoom on one cropper reads the same. A press on Zoom holds it until release. A useZoom change 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:

Option
tag keyof JSX.IntrinsicElements Required
Sets the tag, for example "span"
state S Required
Goes to render and className functions
props object Required
Sets the element's props. Take render and 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