npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-image-studio

v1.2.0

Published

A feature-rich, non-destructive image editor component for React (crop, rotate, filters, brush, text, zoom and export).

Readme

react-image-studio

npm version npm downloads license CI provenance

Live demo → · npm · GitHub

A feature-rich, non-destructive image editor component for React — crop, rotate, flip, colour filters, freehand brush, mosaic redaction, numbered step markers, text layers, zoom/pan and multi-format export. Written in TypeScript, zero runtime dependencies, ships ESM + CJS + type definitions.

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" onSave={(result) => console.log(result.blob)} />
    </div>
  );
}

Features

| Area | What you get | | --- | --- | | Crop | 8-handle crop box, rule-of-thirds guides, drag-to-create, aspect presets (1:1, 4:3, 3:2, 16:9, 3:4, 9:16), lossless pixel-aligned output | | Transform | 90° left/right rotation, horizontal & vertical flip — overlay layers (brush, text) are transformed with the image | | Adjust | Brightness, contrast, saturation, warmth, hue, blur, grayscale, sepia, invert, opacity — double-click a value to reset it | | Filters | 12 live presets (Vivid, Warm, Cool, Mono, Noir, Sepia, Vintage, Dramatic, Fade, Sunset, Matrix…) rendered as real thumbnails of your image | | Brush | Colour, size, opacity and an eraser that only removes paint (never the photo), unlimited strokes with undo | | Mosaic | Brush-based redaction: pixelate (adjustable block size) or blur, adjustable brush, hides photos, painting and text | | Markers | Numbered step badges (circle/square/ring) for tutorials and screenshots, drag to move, auto-numbering + "renumber from top-left" | | Text | Multiple text layers, drag to move, font/size/colour/weight/italic/align/rotation/opacity, double-click to edit | | Navigation | Wheel & pinch zoom to a focal point, drag to pan, fit-to-screen, actual-size, double-click reset | | History | 60-step undo/redo with sensible coalescing (a slider drag is one step) | | Export | PNG (with transparency), JPEG (flattened on white), WebP with quality control, custom file names, clipboard copy | | A11y / i18n | Keyboard shortcuts, ARIA roles & labels, focus rings, prefers-reduced-motion, English + zh-CN labels, fully overridable strings | | Theming | Light/dark/auto, all colours exposed as CSS custom properties |

Everything except crop/rotate is non-destructive: the original bitmap is kept and every layer is re-rendered on demand, so undo never degrades quality.

Install

npm install react-image-studio
# peer dependencies
npm install react react-dom

Usage

Basic

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 — it appends the stylesheet once, which is handy for quick prototypes:

import 'react-image-studio/auto-style';

Controlled props & callbacks

<ImageStudio
  src={file}                 // string | Blob | File
  theme="dark"               // 'light' | 'dark' | 'auto' (default)
  labels="zh-CN"             // or a Partial<Labels> override object
  tools={['crop', 'adjust', 'filter', 'export']}
  defaultTool="crop"
  defaultFormat="webp"
  maxSourceSize={4096}       // downscale huge photos on decode
  onSave={(result) => {
    // result: { blob, format, width, height, quality, filename }
    upload(result.blob);
  }}
  onDocChange={(doc) => setDraft(doc)}
  onError={(error) => toast.error(error.message)}
/>

Imperative API

const editor = useRef<ImageStudioHandle>(null);

<ImageStudio ref={editor} />

// later
await editor.current?.load(newFile);
const blob = await editor.current?.export('png');
editor.current?.rotate(1);
editor.current?.flip('horizontal');
editor.current?.crop({ x: 10, y: 10, width: 800, height: 600 });
editor.current?.undo();
const snapshot = editor.current?.getDoc();   // JSON-serialisable
editor.current?.setDoc(snapshot);

Mosaic and step markers

Both are first-class tools with their own panels, and either can be driven from the hook:

const api = useImageStudio({ src });

api.setMosaic({ style: 'pixelate', block: 18, size: 120 }); // redaction brush
api.setMosaic({ style: 'blur', blur: 14 });

api.setTool('marker');
api.addMarker({ x: 240, y: 180 });   // number assigned automatically
api.renumberMarkers();               // re-sequence 1..n after deletions

Using the state hook directly

The whole editor is driven by a hook, so you can build your own chrome:

import { useImageStudio, buildFilterString } from 'react-image-studio';

function MyEditor() {
  const api = useImageStudio({ src });
  const layer = api.layer; // HTMLCanvasElement with pixels + strokes + text

  return (
    <>
      <canvas
        style={{ filter: buildFilterString(api.doc?.adjust ?? neutral) }}
        ref={(el) => {
          el?.getContext('2d')?.drawImage(layer!, 0, 0);
        }}
      />
      <input
        type="range"
        value={api.doc?.adjust.brightness ?? 100}
        onChange={(e) => api.setAdjust('brightness', Number(e.target.value))}
        onPointerUp={api.endTransient}
      />
      <button onClick={() => api.download('jpeg', 0.9)}>Save</button>
    </>
  );
}

Props

| 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: EditorDoc) => void | – | Fired on every committed change | | onSave | (result: SaveResult) => void | – | Fired after a successful export | | onError | (error: Error) => void | – | Load/export failures | | tools | ToolId[] | all | Which tools to show, in canonical order | | 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 |

Ref API (ImageStudioHandle)

load, export, download, getDoc, setDoc, undo, redo, reset, rotate, flip, crop, setTool, fit, setZoom, canUndo, canRedo.

Keyboard shortcuts

| Keys | Action | | --- | --- | | Ctrl/⌘ + Z | Undo | | Ctrl/⌘ + Shift + Z, Ctrl + Y | Redo | | + / - | Zoom in / out | | 0 | Fit to screen | | Enter | Apply crop | | Esc | Cancel crop / deselect text | | Double click | Fit to screen (or edit the selected text) |

Theming

.my-brand .ris-root {
  --ris-accent: #e11d48;
  --ris-radius: 16px;
  --ris-font: 'Inter', sans-serif;
}

Every colour is a CSS variable (--ris-bg, --ris-surface, --ris-border, --ris-text, --ris-muted, --ris-accent, …), and .ris-theme-dark flips the palette.

Localisation

<ImageStudio labels={{
  app: { ...EN_LABELS.app, download: 'Export image' },
  presets: { ...EN_LABELS.presets, vivid: 'Punchy' },
}} />

Pass labels="zh-CN" for the bundled Chinese translation (or ZH_CN_LABELS to extend it).

Architecture

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 pipeline
    transform.ts   rotate / flip / crop pixel ops
    history.ts     undo stack
    image.ts       loading, encoding, download helpers
  hooks/       useImageStudio — all editor state
  components/  UI (canvas stage, toolbar, panels)
  ImageStudio.tsx

The preview 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.

Browser support

Modern evergreen browsers (Chrome/Edge 88+, Firefox 90+, Safari 15.4+). Canvas ctx.filter is used for adjustments; older engines transparently fall back to a per-pixel implementation for brightness/contrast/saturation/grayscale/sepia/invert.

Remote images are fetched with crossOrigin="anonymous" so exports stay untainted — make sure the server sends Access-Control-Allow-Origin.

Development

npm run build      # ESM + CJS + d.ts + style.css
npm run build:site # docs/ bundle for the GitHub Pages demo
npm run typecheck  # strict tsc, no emit
npm test           # node:test suite (89 tests)
npm run clean

License

MIT © whcater