shadcn
Cropper blocks for shadcn/ui. Each one installs with the shadcn CLI and becomes code in your project.
Before you start
The blocks use the colours, Button and Dialog of your shadcn/ui project, so they follow your theme in light and dark. They work with the Radix and the Base UI styles, and need Tailwind CSS v4 and React 19. The React Aria styles have a different Dialog and Button, so the blocks do not fit them.
Each command adds the files to your project and installs @zriz/react and @zriz/core. Every block returns a File, ready for a form or an upload.
Cropper
The zriz parts in shadcn styles, and the base of every block below. It has the viewport with handles and a grid, a zoom slider, the straighten dial, rotate, flip, reset and an aspect ratio switch.
npx shadcn@latest add https://zriz.dev/r/cropper.jsonPut the parts you need inside Cropper. cropToFile turns the crop into a File:
"use client";
import type { Cropper as CropperApi } from "@zriz/react";
import { useRef } from "react";
import { Button } from "@/components/ui/button";
import {
Cropper,
CropperViewport,
CropperZoom,
CropperZoomIn,
CropperZoomOut,
cropToFile,
} from "@/components/ui/cropper";
type Props = { photo: File; onCrop: (file: File) => void };
export const PhotoCropper = ({ photo, onCrop }: Props) => {
const actions = useRef<CropperApi>(null);
const crop = async () => {
if (actions.current) {
onCrop(
await cropToFile(actions.current, {
type: "image/webp",
})
);
}
};
return (
<Cropper actionsRef={actions} src={photo}>
<CropperViewport />
<div className="flex items-center gap-1">
<CropperZoomOut />
<CropperZoom />
<CropperZoomIn />
</div>
<Button onClick={crop}>Crop</Button>
</Cropper>
);
};Avatar cropper
A profile photo control. The visitor uploads a photo, moves it under a circle in a dialog and saves a round PNG. Adjust opens the same photo again with the last crop.
npx shadcn@latest add https://zriz.dev/r/avatar-cropper.json"use client";
import { AvatarCropper } from "@/components/avatar-cropper";
const upload = async (file: File) => {
const body = new FormData();
body.append("avatar", file);
await fetch("/api/avatar", { method: "POST", body });
};
export const ProfilePhoto = () => (
<AvatarCropper onCrop={upload} />
);Image crop dialog
A dialog you open with any image. It has an aspect ratio switch and a rotate button, and it hands the crop back as a file. Pass your own list of ratios, or one ratio to lock it.
npx shadcn@latest add https://zriz.dev/r/image-crop-dialog.json"use client";
import { useState } from "react";
import { ImageCropDialog } from "@/components/image-crop-dialog";
type Props = { onCrop: (file: File) => void };
export const ProductPhoto = ({ onCrop }: Props) => {
const [photo, setPhoto] = useState<File | null>(null);
return (
<>
<input
aria-label="Product photo"
type="file"
accept="image/*"
onChange={(event) =>
setPhoto(event.target.files?.[0] ?? null)
}
/>
<ImageCropDialog
image={photo}
open={photo !== null}
onOpenChange={(open) => {
if (!open) setPhoto(null);
}}
aspects={[
{ label: "1:1", value: 1 },
{ label: "4:3", value: 4 / 3 },
]}
exportOptions={{ type: "image/webp", quality: 0.9 }}
onCrop={onCrop}
/>
</>
);
};Cover cropper
A wide cover image for a profile or a post. Reposition lets the visitor drag and zoom the image in place, and save exports a JPEG at the width you set.
npx shadcn@latest add https://zriz.dev/r/cover-cropper.json"use client";
import { CoverCropper } from "@/components/cover-cropper";
type Props = { cover: string; onCrop: (file: File) => void };
export const ProfileCover = ({ cover, onCrop }: Props) => (
<CoverCropper
defaultImage={cover}
aspect={3}
width={1500}
onCrop={onCrop}
/>
);Image editor
A full editor in a card: the straighten dial, rotate, flip, five aspect ratios and reset. onSave gets the file and the crop value, so you can store the value and open the editor on it later. Saving state shows how.
npx shadcn@latest add https://zriz.dev/r/image-editor.json"use client";
import type { CropValue } from "@zriz/react";
import { ImageEditor } from "@/components/image-editor";
type Props = {
photo: File;
saved?: CropValue;
onSave: (file: File, value: CropValue) => void;
};
export const PhotoEditor = ({
photo,
saved,
onSave,
}: Props) => (
<ImageEditor
image={photo}
{...(saved ? { defaultValue: saved } : null)}
exportOptions={{ type: "image/jpeg", quality: 0.9 }}
onSave={onSave}
/>
);Image crop field
A form field. The visitor picks or drops an image and frames it in place. The field keeps a file input in sync with the crop, so a plain form or a server action receives the cropped JPEG under name.
npx shadcn@latest add https://zriz.dev/r/image-crop-field.jsonimport { ImageCropField } from "@/components/image-crop-field";
type Props = { action: (data: FormData) => Promise<void> };
export const PostForm = ({ action }: Props) => (
<form action={action}>
<ImageCropField
name="cover"
label="Cover image"
aspect={16 / 9}
/>
<button type="submit">Publish</button>
</form>
);