A free, fully typed image editor for React: crop, adjust, annotate, redact and export, non-destructively.

Almost every app ends up needing an image editor. A profile photo needs cropping, a screenshot needs an arrow and a blurred email address, a product photo needs brightening before it goes live.
The options I found didn’t fit what I wanted: a component that’s free to use in any project, typed end to end, easy to make look like my own app, and ready for Next.js without workarounds.
So I built image-ultra: a free, MIT-licensed image editor component for React and Next.js. Version 0.1 is on npm today.
Try it in your browser: imageultra.ashvattech.com
Five lines to a full editor
npm install @image-ultra/react
import { ImageEditor } from '@image-ultra/react';
import '@image-ultra/react/styles.css';
export default function Page() {
return <div style={{ height: 600 }}><ImageEditor src="/photo.jpg" /></div>;
}That works as-is in a Next.js App Router page. Once you add handlers like onSave, put the editor in your own component that starts with
'use client', because Server Components can't pass functions to the browser.
That’s a complete editor, with these tools:
- Adjust — crop with aspect-ratio presets, rotate, flip, straighten and fix perspective
- Finetune — brightness, contrast, curves, levels, white balance, and a one-tap “Auto”
- Filter — ready-made looks, plus looks your users can save themselves
- Annotate — arrows, shapes, text and images, with layers, grouping and alignment
- Redact — blur or pixelate faces, names and numbers
- Stickers, frames, fills and watermarks
- Resize and export as JPEG, PNG or WebP
Every edit is JSON, not pixels
This is the design choice I care about most. The editor never changes the original photo. Every crop, slider move and arrow is stored as a small, serializable object, the EditState:
<ImageEditor
src={photoUrl}
initialState={savedEdits} // reopen exactly where the user left off
onSave={async (result) => {
await upload(result.blob); // the finished image
await saveEdits(JSON.stringify(result.state)); // the edits that made it
}}
/>
Because the edits are data, you can:
- Reopen an edit later. The user fixes one thing without starting over.
- Re-render without the editor UI, for example to regenerate an image at another size. It runs in the browser, since it needs a canvas:
import { renderImage } from '@image-ultra/react';
const { blob } = await renderImage('/photo.jpg', savedEdits, { mimeType: 'image/webp' });- Store a few kilobytes of JSON next to the original, instead of a new copy of every version.
Undo is unlimited, and every tool has its own reset, so people experiment freely.
Big photos, on phones too
Modern phone cameras take 48-megapixel photos, and a naive canvas editor falls over at that size, especially on iPhones with their strict memory limits.
image-ultra renders with WebGL2 and falls back to Canvas2D where WebGL isn’t available. Sliders update the photo in the same frame you drag them. The preview uses a smaller copy of very large photos, and loads full detail when you zoom in. Exports are always full size: photos bigger than the GPU can hold are exported in tiles.
On an iPhone, editing holds 59 fps with 12 and 24MP photos, and 48MP photos export at full size.
Yours to theme
The editor looks like part of your app, not a third-party widget. Every colour, radius and size is a CSS variable:
.my-editor .iu-root {
--iu-accent: #155e75;
--iu-radius-md: 4px;
}Or use the typed shortcut, which your editor autocompletes:
<ImageEditor theme="light" themeOverrides={{ accent: '#155e75', radius: '12px' }} />It supports dark, light, or following the OS setting. There’s no CSS-in-JS runtime: it’s one plain stylesheet. The docs site has a live theme playground where you pick colours and copy the result.
Built for everyone
- Accessible: it’s fully usable with a keyboard and works with screen readers. It respects reduced motion, and it’s tested with axe against WCAG 2.2 AA.
- Any language: every piece of text is a typed label you can translate, and right-to-left layouts (Arabic, Hebrew) work.
- Typed end to end: TypeScript strict, with no any in the public API.
- Next.js ready: it’s safe for server-side rendering and ships as a Client Component.
A bug I shipped in 0.1.0, and what it taught me about CSS layers
Before announcing a release, I install it from npm into a brand-new app, exactly as a new user would. For 0.1.0, that check found a bug that all of my own tests had missed.
In a fresh create-next-app, the editor looked squashed. Buttons had lost their padding and the Done button was cut off.
The cause is a feature I’d chosen on purpose. All of the editor’s CSS lives in a cascade layer, @layer image-ultra, so that any CSS you write wins without !important. But the cascade treats unlayered CSS as stronger than every layer, whatever its specificity. The default globals.css in create-next-app contains this:
* {
padding: 0;
margin: 0;
}It’s unlayered, so it beat every rule in the editor. Tailwind v3’s reset does the same.
The fix is one unlayered rule that uses a lesser-known CSS keyword:
.iu-root :where(*) {
all: revert-layer;
}Inside the editor, revert-layer rolls every property back to what the cascade layers say, as if the page's unlayered CSS weren't there. all doesn't touch custom properties, so theming with the CSS variables still works exactly as before.
0.1.1 shipped the fix a few hours later. It also added a test that loads the editor, injects the create-next-app and Tailwind resets, and checks that no element in any tool moves or changes.
The lesson: my tests all ran in clean apps, and real apps are never clean. Installing from npm into a fresh app is now a fixed step of every release.
What’s next
0.1 is usable today. The API may still change before 1.0, and your feedback decides how. On the list:
- a Web Component build, so Vue, Svelte and Angular apps can use the editor
- optional AI tools as a separate package: background removal, smart crop and automatic redaction
- faster WebP export
Try it
- Live demo and docs: imageultra.ashvattech.com
- npm: @image-ultra/react
- GitHub: Ultranmus/image-ultra — issues, ideas and stars are all welcome
If you add it to your app, I’d love to see what you build, and if something’s missing, open an issue and tell me.