Crop
Eight drag handles, rule-of-thirds guides, drag-to-create inside the frame and aspect presets (1:1, 4:3, 3:2, 16:9, 3:4, 9:16). Crops stay pixel-aligned, so nothing is resampled.
Crop, rotate, colour-grade, redact with mosaic, and annotate screenshots with numbered step markers — then export PNG, JPEG or WebP. Zero runtime dependencies, TypeScript-first, ESM + CJS, keyboard accessible and fully translatable.
npm install react-image-studio
import { ImageStudio } from 'react-image-studio';
import 'react-image-studio/style.css';
export default function App() {
return (
<div style={{ height: '80vh' }}>
<ImageStudio src="/photo.jpg" />
</div>
);
}
Everything below runs the real component from docs/lib/image-studio.js — the same build that
ships to npm. Draw a mosaic over the sun, then drop 1 · 2 · 3 markers to caption the steps.
Eleven tools, one component, no runtime dependencies.
Eight drag handles, rule-of-thirds guides, drag-to-create inside the frame and aspect presets (1:1, 4:3, 3:2, 16:9, 3:4, 9:16). Crops stay pixel-aligned, so nothing is resampled.
Quarter turns and mirroring. Brush strokes, mosaic strokes, text and markers are remapped into the new coordinate space instead of drifting.
Brightness, contrast, saturation, warmth, hue, blur, grayscale, sepia, invert and opacity. Sliders are real <input type="range"> elements — double-click a value to reset it.
Twelve presets (Vivid, Warm, Cool, Mono, Noir, Sepia, Vintage, Dramatic, Fade, Sunset, Matrix…) rendered as live thumbnails of your image.
Freehand painting with colour, size and opacity — plus an eraser that only removes paint, never the photo underneath.
A dedicated mosaic tool: paint areas you need to hide. Choose pixelate (with block size) or blur (with strength), then adjust the brush diameter. Strokes are stored, not baked.
Click to drop numbered badges 1 · 2 · 3 … for step-by-step screenshots. Circle, square or ring shapes, custom colour and size, drag to reposition, auto-renumbering when you delete one.
Multiple text items with font family, size, weight, italic, colour, alignment, rotation and opacity. Drag on the canvas, double-click to edit.
Sixty steps of history where a whole slider drag, a text edit session or a stroke is one undo step — not one per pointer event.
Wheel and pinch zoom anchored at the pointer, drag to pan, fit-to-screen, actual size, and clamped bounds so the image cannot be lost off-screen.
PNG with transparency, JPEG flattened onto white (no black backgrounds), WebP with a quality slider, custom filenames, plus copy-to-clipboard.
Light / dark / auto themes through CSS custom properties, English and zh-CN strings with full overrides, ARIA roles, visible focus rings and prefers-reduced-motion support.
Four ways in, from “just render it” to “drive it yourself”.
import { ImageStudio } from 'react-image-studio';
import 'react-image-studio/style.css';
<ImageStudio src={fileOrUrl} />
No global CSS import? Use the auto-injecting entry instead:
import 'react-image-studio/auto-style';
<ImageStudio
src={file}
theme="dark" // 'light' | 'dark' | 'auto'
labels="zh-CN" // or Partial<Labels>
tools={['crop', 'mosaic', 'marker', 'export']}
defaultTool="marker"
defaultFormat="webp"
onSave={(result) => upload(result.blob)}
onDocChange={(doc) => setDraft(doc)}
onError={(error) => toast.error(error.message)}
/>
const editor = useRef<ImageStudioHandle>(null);
<ImageStudio ref={editor} />
await editor.current.load(newFile);
const blob = await editor.current.export('png');
editor.current.rotate(1);
editor.current.crop({ x: 10, y: 10, width: 800, height: 600 });
const snapshot = editor.current.getDoc();
import { useImageStudio, buildFilterString } from 'react-image-studio';
const api = useImageStudio({ src });
api.setTool('marker');
api.addMarker({ x: 200, y: 140 }); // numbered automatically
api.setMosaic({ style: 'blur', blur: 14 });
api.renumberMarkers();
await api.download('webp', 0.85);
The hook exposes the exact same state the built-in UI uses.
Props, the imperative handle, and the hook faces added in 1.1.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | Blob | File | null | – | Image to load (URL, data URL, blob or file). |
image | CanvasImageSource | – | Alternative entry point for an already decoded image. |
doc | EditorDoc | – | Restore a previously saved document. |
onDocChange | (doc) => void | – | Fired on every committed change. |
onSave | (result) => void | – | Fired after a successful export. |
onError | (error) => void | – | Load / export failures. |
tools | ToolId[] | all | move · crop · rotate · adjust · filter · brush · mosaic · text · marker · export |
defaultTool | ToolId | 'move' | Tool selected on mount. |
theme | 'light' | 'dark' | 'auto' | 'auto' | Chrome colour scheme. |
labels | 'zh-CN' | Partial<Labels> | English | UI strings. |
className / style | – | – | Applied to the root element. |
defaultFormat | 'png' | 'jpeg' | 'webp' | 'png' | Initial export format. |
maxSourceSize | number | 0 | Longest edge used when decoding (0 = keep original). |
allowDrop | boolean | true | Enable drag & drop loading. |
disableShortcuts | boolean | false | Turn off keyboard shortcuts. |
emptyState | ReactNode | – | Custom empty state. |
load · export · download · getDoc · setDoc ·
undo · redo · reset · rotate · flip ·
crop · setTool · fit · setZoom · canUndo ·
canRedo
| Face | Signature | Description |
|---|---|---|
mosaic | MosaicSettings | { size, style: 'pixelate' | 'blur', block, blur } |
setMosaic | (patch: Partial<MosaicSettings>) => void | Update brush diameter, style, block size or blur strength. |
clearMosaic | () => void | Remove every mosaic stroke (brush paint is kept). |
marker | MarkerSettings | { color, size, shape: 'circle' | 'square' | 'ring' } |
setMarker | (patch: Partial<MarkerSettings>) => void | Defaults for the next marker. |
addMarker | (patch?: Partial<Marker>) => Marker | null | Drop a marker; the number is assigned for you. |
updateMarker | (id, patch, merge?) => void | Move or restyle one marker. |
removeMarker | (id: string) => void | Delete a marker. |
renumberMarkers | () => void | Re-sequence the badges as 1..n. |
clearMarkers | () => void | Remove all markers. |
selectedMarkerId / selectMarker | string | null / (id) => void | Selection state for custom panels. |
Every colour is a CSS custom property, so the whole chrome can be re-skinned from the outside. .ris-theme-dark flips the palette; prefers-color-scheme drives the auto mode.
.my-brand .ris-root {
--ris-accent: #e11d48;
--ris-radius: 16px;
--ris-font: 'Inter', sans-serif;
}
Tokens: --ris-bg --ris-surface --ris-surface-2
--ris-surface-3 --ris-border --ris-text --ris-muted
--ris-accent --ris-accent-hover --ris-accent-contrast
--ris-danger --ris-radius --ris-shadow --ris-font
Pass 'zh-CN' for the bundled translation, or override any string:
<ImageStudio labels={{
app: { ...EN_LABELS.app, download: 'Export image' },
presets: { ...EN_LABELS.presets, vivid: 'Punchy' },
}} />
Mosaic and marker tool labels are part of the same Labels object.
The preview layer is rendered once per structural change; colour adjustments are applied as a CSS filter while blitting, so dragging a slider never re-renders the bitmap. Export always re-renders at full resolution, which is why every tool is non-destructive.
src/
core/ framework-free engine (no React imports)
geometry.ts crop maths, hit testing, view transform
filters.ts adjustments, CSS filter string, presets
render.ts layer compositing (base → mosaic → paint → text → markers)
transform.ts rotate / flip / crop pixel ops
history.ts undo stack
image.ts loading, encoding, download helpers
hooks/ useImageStudio — all editor state
components/ canvas stage, toolbar, panels
ImageStudio.tsx
Browser support: Chrome/Edge 88+, Firefox 90+, Safari 15.4+. Canvas ctx.filter is used for
adjustments, with a per-pixel fallback where it is missing. Remote images load with
crossOrigin="anonymous" so exports stay untainted.