Skip to navigation

Aspect ratio

Buttons that lock the crop to a ratio such as 1:1 or 4:5, and one that swaps portrait and landscape.

Open
Download
FreeOriginal1:14:516:9

Anatomy

The group and the orientation button go anywhere inside Root. Each item names one ratio:

<Cropper.AspectGroup aria-label="Aspect ratio">
  <Cropper.AspectItem value="free">Free</Cropper.AspectItem>
  <Cropper.AspectItem value={1}>1:1</Cropper.AspectItem>
  <Cropper.AspectItem value={4 / 5}>4:5</Cropper.AspectItem>
  <Cropper.AspectItem value={16 / 9}>16:9</Cropper.AspectItem>
</Cropper.AspectGroup>
<Cropper.Orientation aria-label="Swap orientation">
  <svg
    aria-hidden="true"
    width={20}
    height={20}
    viewBox="0 0 20 20"
  >
    <path
      d="M3 6h14v8H3z"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.5}
    />
  </svg>
</Cropper.Orientation>

value is width divided by height, so 4 / 5 is 4:5. "free" drops the lock and "original" uses the photo's own ratio. Picking an item springs the crop to it, with data-transition="morph" on Root.

On a round crop every item but value={1} is disabled. zriz ships these parts bare. The demo above adds "original", turns its labels with the orientation as the ratios further down do, and uses the styles from the zriz site.

Lock one ratio with no buttons

Pass aspect to Root and leave out the group. The crop keeps that ratio, and Reset returns to it:

<Cropper.Root src={photo} aspect={16 / 9}>
  <Cropper.Viewport style={{ height: 400 }}>
    <Cropper.Image />
    <Cropper.Mask />
    <Cropper.Area aria-label="Crop area" />
  </Cropper.Viewport>
</Cropper.Root>

Use an item outside a group

Outside AspectGroup, an item is a plain button with aria-pressed instead of a radio:

<Cropper.AspectItem value={1}>Square</Cropper.AspectItem>

Turn the orientation icon

Orientation says which way the crop faces in data-orientation. This turns its icon for portrait:

[data-zriz-item="orientation"] {
  &[data-orientation="portrait"] svg {
    rotate: 90deg;
  }
}

A press swaps the ratio for its inverse, so 4:5 becomes 5:4. It springs like an item pick, and onValueChange gets the reason orientation.

Follow the orientation in the labels

After a swap the crop is 5:4, so a 4:5 item is no longer checked. To keep one item per ratio, flip the values and labels with the orientation. Render it inside Root:

"use client";

import { useCropperState } from "@zriz/react";
import * as Cropper from "@zriz/react/cropper";

const RATIOS = [
  [4, 5],
  [3, 4],
  [9, 16],
] as const;

export const Ratios = () => {
  const aspect = useCropperState((s) => s.aspect);
  const wide = typeof aspect === "number" && aspect > 1;

  return (
    <Cropper.AspectGroup aria-label="Aspect ratio">
      <Cropper.AspectItem value="free">Free</Cropper.AspectItem>
      <Cropper.AspectItem value={1}>1:1</Cropper.AspectItem>
      {RATIOS.map(([w, h]) => (
        <Cropper.AspectItem
          key={`${w}:${h}`}
          value={wide ? h / w : w / h}
        >
          {wide ? `${h}:${w}` : `${w}:${h}`}
        </Cropper.AspectItem>
      ))}
    </Cropper.AspectGroup>
  );
};

Keyboard

The group is one tab stop. Tab lands on the checked item, or on the first one when none is checked. The arrows move and select at once:

KeyWhat it does
rightordownSelect the next ratio
leftorupSelect the previous ratio
homeorendSelect the first or the last ratio

The arrows wrap around at the ends and skip disabled items. On a right-to-left page or with dir="rtl" on Root, left and right swap.

API

AspectGroup

Renders a <div role="radiogroup"> anywhere inside Root. Every part also takes className, render and the props of its element, see composition. className and render get { aspect }.

Prop
aria-label string
Names the group for screen readers

AspectItem

Renders a <button type="button">, with role="radio" inside AspectGroup. className and render get { checked, disabled }.

Prop
value "free" | "original" | number Required
The ratio this item picks, as width / height
AttributeWhen
data-checkedWhile this ratio is the current one
data-disabledOn a round crop for every value but 1, or when you pass disabled to the item

Orientation

Renders a <button type="button"> anywhere inside Root. It has no props of its own. It is disabled while the aspect is 1, "free" or "original". className and render get { orientation }.

AttributeWhen
data-orientation"landscape" above 1, "portrait" below 1, "square" at 1 and while the aspect is "free" or "original"
data-disabledWhile the aspect is 1, "free" or "original"