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
├── ImageCropperViewport
├── ImageCropperImage
├── ImageCropperSelection
├── ImageCropperHandle
└── ImageCropperGridUsage
import {
ImageCropper,
ImageCropperImage,
ImageCropperSelection,
} from "@/components/ui/image-cropper";<ImageCropper className="aspect-video w-full max-w-lg">
<ImageCropperImage
alt="Crop me"
src="https://images.unsplash.com/photo-1662692735672-544412d65934?w=600&auto=format"
/>
<ImageCropperSelection />
</ImageCropper>Demos
Aspect Ratio
Circle Crop
Controlled Zoom
Fixed Crop Area
Initial Crop
Min Max Size
Zoom Limits
API Reference
ImageCropper
div
PropType
AttributeType
CSS variableType
ImageCropperViewport
div
PropType
CSS variableType
ImageCropperImage
img
PropType
AttributeType
ImageCropperSelection
div
PropType
AttributeType
CSS variableType
ImageCropperHandle
div
PropType
AttributeType
CSS variableType
ImageCropperGrid
div
PropType
AttributeType