shadcn.io is not affiliated with official shadcn/ui
Shadcn Image Cropper for React and Tailwind
Crop and transform images.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/image-cropper.jsonpnpm dlx shadcn@latest add https://kit.dev/r/image-cropper.jsonnpx shadcn@latest add https://kit.dev/r/image-cropper.jsonyarn shadcn@latest add https://kit.dev/r/image-cropper.jsonInstall the following dependencies:
bun add @ark-ui/reactpnpm add @ark-ui/reactnpm install @ark-ui/reactyarn add @ark-ui/reactCopy and paste the following code into your project.
"use client";
import {
ImageCropper as ArkImageCropper,
useImageCropper as useArkImageCropper,
} from "@ark-ui/react/image-cropper";
import type React from "react";
import { cn } from "@/lib/utils";
export const useImageCropper = useArkImageCropper;
interface ImageCropperProps
extends React.ComponentProps<typeof ArkImageCropper.Root> {}
export const ImageCropper = (props: ImageCropperProps) => {
const { className, children, ...rest } = props;
return (
<ArkImageCropper.Root
className={cn(
"[--cropper-accent:var(--color-white)] [--cropper-handler-size:--spacing(2)] [--cropper-handler-width:--spacing(1)]",
"relative",
"w-full",
"aspect-video",
className
)}
data-slot="image-cropper"
{...rest}
>
<ArkImageCropper.Viewport
className={cn("size-full", "overflow-hidden")}
data-slot="image-cropper-viewport"
>
{children}
</ArkImageCropper.Viewport>
</ArkImageCropper.Root>
);
};
export const ImageCropperImage = (
props: React.ComponentProps<typeof ArkImageCropper.Image>
) => {
const { className, ...rest } = props;
return (
<ArkImageCropper.Image
className={cn(
"absolute top-0 left-0",
"size-full object-contain",
"select-none",
"backface-hidden",
"pointer-events-none",
"origin-center",
className
)}
data-slot="image-cropper-image"
{...rest}
/>
);
};
interface ImageCropperSelectionProps
extends React.ComponentProps<typeof ArkImageCropper.Selection> {
/**
* The axis of the grid to show.
*
* @default "both"
*/
axis?: "horizontal" | "vertical" | "both";
}
export const ImageCropperSelection = (props: ImageCropperSelectionProps) => {
const { axis = "both", className, children, ...rest } = props;
return (
<ArkImageCropper.Selection
className={cn(
"shadow-[0_0_0_9999px_rgb(0_0_0/0.5)]",
"border-2 border-white/64",
"backface-visibility-hidden",
"cursor-move",
"data-[shape=circle]:rounded-full",
"outline-none focus-visible:border-(--cropper-accent)",
"data-disabled:cursor-default",
"data-dragging:cursor-grabbing data-dragging:border-white/84",
className
)}
data-slot="image-cropper-selection"
{...rest}
>
{children}
{(axis === "horizontal" || axis === "both") && (
<ImageCropperGrid axis="horizontal" />
)}
{(axis === "vertical" || axis === "both") && (
<ImageCropperGrid axis="vertical" />
)}
<ImageCropperHandle position="n" />
<ImageCropperHandle position="e" />
<ImageCropperHandle position="s" />
<ImageCropperHandle position="w" />
<ImageCropperHandle position="ne" />
<ImageCropperHandle position="se" />
<ImageCropperHandle position="sw" />
<ImageCropperHandle position="nw" />
</ArkImageCropper.Selection>
);
};
export const ImageCropperHandle = (
props: React.ComponentProps<typeof ArkImageCropper.Handle>
) => {
const { className, ...rest } = props;
return (
<ArkImageCropper.Handle
className={cn(
"absolute flex touch-none items-center justify-center",
"h-[calc(var(--cropper-handler-size)+8px)] w-[calc(var(--cropper-handler-size)+8px)]",
"data-disabled:hidden",
"data-[position=ne]:cursor-nesw-resize data-[position=nw]:cursor-nwse-resize",
"data-[position=se]:cursor-nwse-resize data-[position=sw]:cursor-nesw-resize",
"data-[position=n]:cursor-ns-resize data-[position=s]:cursor-ns-resize",
"data-[position=e]:cursor-ew-resize data-[position=w]:cursor-ew-resize",
"border-(--cropper-accent)",
"[&>span]:bg-(--cropper-accent) [&>span]:shadow-[0_1px_3px_rgb(0_0_0/0.3)]",
"data-[position=nw]:hover:**:scale-110 [&[data-position=nw]_*]:size-(--cropper-handler-size) [&[data-position=nw]_*]:border-t-[length:(--cropper-handler-width)] [&[data-position=nw]_*]:border-l-[length:(--cropper-handler-width)] [&[data-position=nw]_*]:bg-(--cropper-accent)",
"data-[position=ne]:hover:**:scale-110 [&[data-position=ne]_*]:size-(--cropper-handler-size) [&[data-position=ne]_*]:border-t-[length:(--cropper-handler-width)] [&[data-position=ne]_*]:border-r-[length:(--cropper-handler-width)] [&[data-position=ne]_*]:bg-(--cropper-accent)",
"data-[position=se]:hover:**:scale-110 [&[data-position=se]_*]:size-(--cropper-handler-size) [&[data-position=se]_*]:border-r-[length:(--cropper-handler-width)] [&[data-position=se]_*]:border-b-[length:(--cropper-handler-width)] [&[data-position=se]_*]:bg-(--cropper-accent)",
"data-[position=sw]:hover:**:scale-110 [&[data-position=sw]_*]:size-(--cropper-handler-size) [&[data-position=sw]_*]:border-b-[length:(--cropper-handler-width)] [&[data-position=sw]_*]:border-l-[length:(--cropper-handler-width)] [&[data-position=sw]_*]:bg-(--cropper-accent)",
"[&[data-position=n]_*]: data-[position=n]:hover:**:opacity-100 [&[data-position=n]_*]:size-1.5 [&[data-position=n]_*]:opacity-0",
"data-[position=s]:hover:**:opacity-100 [&[data-position=s]_*]:size-1.5 [&[data-position=s]_*]:bg-(--cropper-accent) [&[data-position=s]_*]:opacity-0",
"data-[position=e]:hover:**:opacity-100 [&[data-position=e]_*]:size-1.5 [&[data-position=e]_*]:bg-(--cropper-accent) [&[data-position=e]_*]:opacity-0",
"data-[position=w]:hover:**:opacity-100 [&[data-position=w]_*]:size-1.5 [&[data-position=w]_*]:bg-(--cropper-accent) [&[data-position=w]_*]:opacity-0",
className
)}
data-slot="image-cropper-handle"
{...rest}
>
<span aria-hidden className="block size-(--cropper-handler-size)" />
</ArkImageCropper.Handle>
);
};
export const ImageCropperGrid = (
props: React.ComponentProps<typeof ArkImageCropper.Grid>
) => {
const { className, ...rest } = props;
return (
<ArkImageCropper.Grid
className={cn(
"absolute",
"opacity-0",
"pointer-events-none",
"transition-opacity duration-200",
"data-[axis=horizontal]:inset-[33.33%_0] data-[axis=horizontal]:border-white/40 data-[axis=horizontal]:border-t data-[axis=horizontal]:border-b",
"data-[axis=vertical]:inset-0_[33.33%] data-[axis=vertical]:border-white/40 data-[axis=vertical]:border-r data-[axis=vertical]:border-l",
"data-dragging:opacity-100",
"data-panning:opacity-100",
"motion-reduce:transition-none!",
className
)}
data-slot="image-cropper-grid"
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
ImageCropper
├── ImageCropperImage
├── ImageCropperSelection
├── ImageCropperHandle
└── ImageCropperGridUsage
import {
ImageCropper,
ImageCropperImage,
ImageCropperSelection,
} from "@/components/ui/image-cropper";<ImageCropper>
<ImageCropperImage alt="Crop me" src="/image.jpg" />
<ImageCropperSelection />
</ImageCropper>Examples
Aspect ratio
Use the aspectRatio to lock the crop area to a specific aspect ratio.
Circle crop
Use cropShape="circle" for profile pictures or avatars.
Initial crop
Start with a pre-defined crop area using the initialCrop prop.
Controlled zoom
Control zoom programmatically with the zoom and onZoomChange props.
Zoom limits
Set minZoom and maxZoom to constrain how far users can zoom.
Min and max size
Constrain the crop area size with minWidth, minHeight, maxWidth, and maxHeight.
Fixed crop area
Set fixedCropArea to true when the crop area should stay fixed while the image moves underneath.
API Reference
ImageCropper
Root wrapper. Manages crop state and handles.
| Prop | Type | Default |
|---|---|---|
aspectRatio | number | - |
cropShape | "circle" | "rectangle" | "rectangle" |
fixedCropArea | boolean | false |
initialCrop | { x: number; y: number; width: number; height: number } | - |
maxHeight | number | Infinity |
maxWidth | number | Infinity |
maxZoom | number | 5 |
minHeight | number | 40 |
minWidth | number | 40 |
minZoom | number | 1 |
onCropChange | (details: CropChangeDetails) => void | - |
onZoomChange | (details: ZoomChangeDetails) => void | - |
rotation | number | - |
zoom | number | - |
className | string | - |
| Attribute | Default |
|---|---|
--cropper-accent | var(--color-white) |
--cropper-handler-size | --spacing(2) |
--cropper-handler-width | --spacing(1) |
ImageCropperImage
Image to crop. Pass src and alt.
| Prop | Type | Default |
|---|---|---|
alt | string | - |
src | string | - |
className | string | - |
ImageCropperSelection
Crop selection overlay with resize handles and optional grid.
| Prop | Type | Default |
|---|---|---|
axis | "horizontal" | "vertical" | "both" | "both" |
className | string | - |
For a complete list of props, see the Ark UI documentation.