A photo editor you own, one component at a time.
photocn is a WebGL photo editor distributed the shadcn way. Install the complete editor with one command, or pick the pieces you need and compose them like Lego. Rendering runs off the main thread, so sliders stay smooth on 24-megapixel photos.
pnpm dlx shadcn@latest add https://photocn.dev/r/image-editor.json"use client";
import { ImageEditor } from "@/components/image-editor/image-editor";
export default function FullEditorExample() {
return <ImageEditor className="h-[680px] overflow-hidden rounded-xl border" src="/samples/mountain-lake.jpg" />;
}Drop in the whole editor
One command gives you the full editor: canvas, curves, filters, crop, blur, blend, EXIF and export. It uses your theme and your primitives.
<ImageEditor src={file} />Snap the pieces together
Every panel, button and canvas is a separate component that reads the nearest provider. Arrange them however you like.
<ImageEditorProvider> <ImageEditorCanvas /> <ImageEditorFilters /> </ImageEditorProvider>
Go fully headless
useImageEditor() exposes params, history, crop, filters and export. Build any UI you want on top of it.
const editor = useImageEditor()
editor.filters.select("juno")Build a different editor with the same parts
This phone-style filter picker is the canvas component plus a dozen lines of your own JSX on top of useImageEditor().
"use client";
import { ImageEditorProvider, useImageEditor } from "photocn/react";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/lib/utils";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";
export default function FilterStripExample() {
return (
<ImageEditorProvider src="/samples/strawberries.jpg">
<div className="mx-auto flex h-[640px] max-w-sm flex-col overflow-hidden rounded-3xl border bg-background shadow-sm">
<ImageEditorCanvas className="min-h-0 flex-1 p-2" showOpenButton={false} />
<FilterStrip />
</div>
</ImageEditorProvider>
);
}
// A custom control built only from `useImageEditor()` — no photocn panel.
function FilterStrip() {
const { filters, isReady } = useImageEditor();
return (
<div className="flex flex-col gap-3 border-t p-3">
<div className="flex gap-2 overflow-x-auto pb-1">
<FilterChip
active={filters.value.label === null}
disabled={!isReady}
label="Original"
onClick={() => void filters.select(null)}
/>
{filters.presets.map((preset) => (
<FilterChip
active={filters.value.label === preset.label}
disabled={!isReady || filters.loading !== null}
key={preset.label}
label={preset.label}
onClick={() => void filters.select(preset)}
/>
))}
</div>
{filters.value.label ? (
<Slider
aria-label="Filter strength"
max={100}
onValueChange={([value]) => filters.setStrength((value ?? 0) / 100)}
value={[Math.round(filters.value.strength * 100)]}
/>
) : null}
</div>
);
}
function FilterChip({
active,
label,
...props
}: { active: boolean; label: string } & React.ComponentProps<typeof Button>) {
return (
<Button
aria-pressed={active}
className={cn("shrink-0 rounded-full capitalize", active && "shadow-sm")}
size="sm"
variant={active ? "default" : "outline"}
{...props}
>
{label}
</Button>
);
}