Compose your own
Wrap any layout in a provider and place the pieces wherever you want.
How it fits together
<ImageEditorProvider> owns the image, the edit params and the undo history. Every ImageEditor* component below it reads the provider through useImageEditor(), so they stay in sync without any wiring from you.
<ImageEditorProvider src={file} onParamsChange={save}>
<ImageEditorCanvas /> {/* the image */}
<ImageEditorAdjustments /> {/* any panel */}
<ImageEditorUndoButton /> {/* any control */}
<MyOwnThing /> {/* useImageEditor() inside */}
</ImageEditorProvider>A compact editor
Canvas, adjustments, history, compare and export: five pieces.
"use client";
import { ImageEditorProvider } from "photocn/react";
import { ImageEditorAdjustments } from "@/components/image-editor/adjustments-panel";
import { ImageEditorCanvas } from "@/components/image-editor/canvas";
import { ImageEditorExportDialog } from "@/components/image-editor/export-dialog";
import {
ImageEditorCompareButton,
ImageEditorRedoButton,
ImageEditorUndoButton,
} from "@/components/image-editor/toolbar";
export default function ComposeYourOwnExample() {
return (
<ImageEditorProvider src="/samples/street.jpg">
<div className="grid h-[560px] grid-rows-[auto_1fr] overflow-hidden rounded-xl border bg-background md:grid-cols-[1fr_300px]">
<header className="flex items-center gap-1 border-b p-2 md:col-span-2">
<ImageEditorUndoButton />
<ImageEditorRedoButton />
<div className="ml-auto flex gap-2">
<ImageEditorCompareButton />
<ImageEditorExportDialog />
</div>
</header>
<ImageEditorCanvas className="min-h-64 p-3" />
<aside className="overflow-y-auto border-t md:border-t-0 md:border-l">
<ImageEditorAdjustments />
</aside>
</div>
</ImageEditorProvider>
);
}Your own controls
The filter strip below is custom JSX. It lists editor.filters.presets, calls editor.filters.select(), and renders the shared canvas.
"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>
);
}Controlled pieces
Each panel file also exports its plain controlled version (AdjustmentPanel, CurvesPanel, FiltersPanel, and so on) that takes value and onChange. Use those to drive a panel from your own state, or outside a provider.