General-purpose mask editor for React image manipulation apps
npm install react-canvas-masker> ๐๏ธ A lightweight, flexible React component and hook for drawing and extracting masks from images using canvas. Perfect for AI workflows, in-browser image editing tools, and selective manipulation.
---
react-canvas-masker?react-canvas-masker is a modern and actively maintained React library that allows users to draw freeform masks over images, extract those masked regions, and integrate with AI-powered image processing workflows or any kind of canvas-based editing tool.
Itโs built as an enhanced fork of react-mask-editor, rewritten with:
- โ
Hook-based architecture
- ๐ Undo/redo support
- ๐ง Flexible API
- ๐งผ Clean and modern codebase
---
- โ
Draw 1-bit (black/white) masks over any image using a brush tool
- ๐ Undo/redo and clear support
- ๐จ Customizable brush: size, color, opacity, blend mode
- ๐ Zoom and pan capabilities for precise mask editing
- ๐ฑ๏ธ Intuitive controls: mouse wheel zoom, space+drag panning
- ๐ฆ Use as a component, hook, or via React context
- โก Imperative API via ref
- ๐ฑ Responsive design that adapts to container size
- ๐งช Local demo/example app included
---
``bash`
npm install react-canvas-maskeror
yarn add react-canvas-masker
---
`tsx
import React from 'react';
import { MaskEditor, toMask } from 'react-canvas-masker';
const MyComponent = () => {
const canvas = React.useRef(null);
return (
<>
onClick={() => {
if (canvas.current?.maskCanvas) {
console.log(toMask(canvas.current.maskCanvas));
}
}}
>
Get Mask
>
);
};
`
You can resume editing from a previously saved mask by passing it as the initialMask prop:
`tsx
import React from 'react';
import { MaskEditor, toMask } from 'react-canvas-masker';
const MyComponent = () => {
const canvas = React.useRef(null);
const [savedMask, setSavedMask] = React.useState(null);
return (
<>
canvasRef={canvas}
initialMask={savedMask} // Load previously saved mask
onMaskChange={(mask) => {
// Auto-save mask on changes
localStorage.setItem('myMask', mask);
}}
/>
onClick={() => {
if (canvas.current?.maskCanvas) {
const mask = toMask(canvas.current.maskCanvas);
setSavedMask(mask);
localStorage.setItem('myMask', mask);
}
}}
>
Save Mask
onClick={() => {
const loadedMask = localStorage.getItem('myMask');
if (loadedMask) {
setSavedMask(loadedMask);
}
}}
>
Load Saved Mask
>
);
};
`
---
| Prop | Type | Required | Default | Description | | | | | | | | | | | | | | | |
| -------------------- | -------------------------------- | ---------- | --------- | -------------------------------------------------------------------------------------------- | -------- | --------- | ------------- | ------------ | ------------ | ------------ | ------------ | ----------- | ----- | ------------ | ------- | -------------- | --- | -------- | ---------------------------------------------------------------------------------------------------- |
| src | string | Yes | โ | Source URL of the image to edit. | | | | | | | | | | | | | | | |cursorSize
| | number | No | 10 | Radius (in pixels) of the brush for editing the mask. | | | | | | | | | | | | | | | |onCursorSizeChange
| | (size: number) => void | No | โ | Callback when the user changes the brush size via mouse wheel. | | | | | | | | | | | | | | | |maskOpacity
| | number | No | 0.4 | CSS opacity, decimal between 0โ1. | | | | | | | | | | | | | | | |maskColor
| | string | No | #ffffff | Hex color (with or without leading '#') for the mask. | | | | | | | | | | | | | | | |maskBlendMode
| | \"normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity"\ | No | normal | CSS blending mode for the mask layer. |onDrawingChange
| | (isDrawing: boolean) => void | Yes | โ | Called when the user starts or stops drawing. | | | | | | | | | | | | | | | |maxWidth
| | number | No | 1240 | Maximum width for loaded images. Images larger than this will be scaled down automatically. | | | | | | | | | | | | | | | |maxHeight
| | number | No | 1240 | Maximum height for loaded images. Images larger than this will be scaled down automatically. | | | | | | | | | | | | | | | |crossOrigin
| | string | No | โ | Value for the crossOrigin attribute on the underlying . Useful for CORS images. | | | | | | | | | | | | | | | |onUndoRequest
| | () => void | No | โ | Called when the user requests an undo action. | | | | | | | | | | | | | | | |onRedoRequest
| | () => void | No | โ | Called when the user requests a redo action. | | | | | | | | | | | | | | | |onMaskChange
| | (mask: string) => void | No | โ | Called with the current mask (as a dataURL) when the mask changes. Debounced while drawing. | | | | | | | | | | | | | | | |initialMask
| | string | No | โ | Pre-load an existing mask as base64 data URL. Useful for resuming editing from a saved state. | | | | | | | | | | | | | | | |scale
| | number | No | 1 | Initial zoom scale for the image editor. | | | | | | | | | | | | | | | |minScale
| | number | No | 0.8 | Minimum allowed zoom scale. | | | | | | | | | | | | | | | |maxScale
| | number | No | 4 | Maximum allowed zoom scale. | | | | | | | | | | | | | | | |onScaleChange
| | (scale: number) => void | No | โ | Callback when the zoom scale changes. | | | | | | | | | | | | | | | |enableWheelZoom
| | boolean | No | true | Enable/disable zooming with the mouse wheel. | | | | | | | | | | | | | | | |onPanChange
| | (x: number, y: number) => void | No | โ | Callback when the pan position changes. | | | | | | | | | | | | | | | |constrainPan
| | boolean | No | true | Enable/disable constraints that keep the image in view while panning. | | | | | | | | | | | | | | | |
---
)The MaskEditor component exposes useful methods via ref:
| Name | Type | Description | |
| ------------- | -------------------------------- | ------------------------------------------------- | ------------------------ |
| maskCanvas | \HTMLCanvasElement | null\ | The mask canvas element. |undo()
| | () => void | Undo the last mask change. | |redo()
| | () => void | Redo the last undone mask change. | |clear()
| | () => void | Clear the mask. | |resetZoom()
| | () => void | Reset zoom to initial scale and center the image. | |setPan()
| | (x: number, y: number) => void | Set the pan position manually. | |zoomIn()
| | () => void | Zoom in by one step (0.1 scale increment). | |zoomOut()
| | () => void | Zoom out by one step (0.1 scale decrement). | |
---
You can manage the full mask editing flow yourself:
`tsxZoom level: ${newScale}
const CustomMaskEditor = () => {
const {
canvasRef,
clear,
cursorCanvasRef,
handleMouseDown,
handleMouseUp,
key,
maskBlendMode,
maskCanvasRef,
maskOpacity,
redo,
transform,
effectiveScale,
size,
undo,
containerRef,
resetZoom,
isPanning,
setPan,
} = useMaskEditor({
src: 'https://placekitten.com/256/256',
maskColor: '#00ff00',
maxWidth: 1024, // Optional: limit image width
maxHeight: 1024, // Optional: limit image height
onDrawingChange: (drawing) => console.log(drawing),
// Zoom and pan options
scale: 1, // Initial scale
minScale: 0.5, // Minimum zoom allowed
maxScale: 5, // Maximum zoom allowed
enableWheelZoom: true, // Enable mouse wheel zoom
constrainPan: true, // Keep image in view while panning
onScaleChange: (newScale) => console.log(),Pan position: ${x}, ${y}
onPanChange: (x, y) => console.log(),
});
const transformStyle = React.useMemo(() => {
return {
position: 'absolute' as const,
top: '50%',
left: '50%',
transform: translate(-50%, -50%) scale(${effectiveScale}) translate(${transform.translateX}px, ${transform.translateY}px),
transformOrigin: 'center',
transition: isPanning ? 'none' : 'transform 0.15s ease-out',
width: size.x + 'px',
height: size.y + 'px',
display: 'block',
};
}, [transform, effectiveScale, isPanning, size]);
return (
,
maxHeight: ${1024}px,
minHeight: '300px',
width: '100%',
height: '100%',
}}
tabIndex={0}
>
className="react-mask-editor-inner"
ref={containerRef}
style={{
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'relative',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
className="canvas-container"
style={{
position: 'relative',
maxWidth: '100%',
maxHeight: '100%',
width: '100%',
height: '100%',
minHeight: '200px',
overflow: 'hidden',
}}
>
key={key}
ref={canvasRef}
style={{
width: size.x,
height: size.y,
}}
width={size.x}
height={size.y}
className="react-mask-editor-base-canvas"
/>
ref={maskCanvasRef}
width={size.x}
height={size.y}
style={{
width: size.x,
height: size.y,
opacity: maskOpacity,
mixBlendMode: maskBlendMode as any,
}}
className="react-mask-editor-mask-canvas"
/>
ref={cursorCanvasRef}
width={size.x}
height={size.y}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
style={{
width: size.x,
height: size.y,
cursor: isPanning ? 'grabbing' : 'default',
}}
className="react-mask-editor-cursor-canvas"
/>
$3
Ideal if you want to split canvas and controls across components:
`tsx
import { MaskEditorProvider, useMaskEditorContext } from 'react-canvas-masker';const MaskEditorCanvas = () => {
const {
canvasRef,
maskCanvasRef,
cursorCanvasRef,
containerRef,
size,
transform,
isPanning,
handleMouseDown,
handleMouseUp,
} = useMaskEditorContext();
return (
ref={containerRef}
style={{ width: '100%', height: '500px', position: 'relative' }}
>
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: translate(-50%, -50%) scale(${transform.scale}) translate(${transform.translateX}px, ${transform.translateY}px),
transition: isPanning ? 'none' : 'transform 0.15s ease-out',
}}
>
ref={cursorCanvasRef}
width={size.x}
height={size.y}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
/>
const MaskEditorControls = () => {
const { undo, redo, clear, resetZoom, setPan, scale, zoomIn, zoomOut } =
useMaskEditorContext();
return (
const App = () => (
maxWidth={1024} // Optional: limit image width
maxHeight={1024} // Optional: limit image height
crossOrigin="anonymous" // Optional: set crossOrigin for CORS
onDrawingChange={() => {}}
// Zoom and pan options
scale={1}
minScale={0.5}
maxScale={5}
enableWheelZoom={true}
constrainPan={true}
onScaleChange={(scale) => console.log(Zoom: ${scale})}Pan: ${x}, ${y}
onPanChange={(x, y) => console.log()}`
>
);
---
The editor includes sophisticated zoom and pan capabilities to enable precise mask editing:
- Zoom: Use Ctrl/Cmd + Mouse Wheel to zoom in/out centered on imageSpace
- Pan: Hold and drag to pan the image, or use middle mouse buttonMouse Wheel
- Resize Brush: Use (without modifier keys) to adjust brush size
The editor now provides explicit zoom control methods through the imperative API:
- zoomIn(): Increases zoom by 0.1 scale increment (respects maxScale limit)
- zoomOut(): Decreases zoom by 0.1 scale decrement (respects minScale limit)
- resetZoom(): Resets zoom to scale 1 and centers the image
- setPan(x, y): Manually sets the pan position
These methods can be accessed through:
- Component ref: maskEditorRef.current.zoomIn()const { zoomIn } = useMaskEditorContext()
- Context: const { zoomIn } = useMaskEditor(props)
- Hook:
Perfect for implementing custom toolbar zoom controls with buttons or sliders!
- Responsive Scaling: Images automatically scale to fit their container
- Smooth Transitions: Gentle animations when zooming (disabled during active panning)
- Position Constraints: Optional boundaries prevent the image from being panned too far out of view
- Centered Reset: resetZoom() function centers the image and resets scale to 1
`tsx
// Example of programmatically controlling zoom and pan
const CustomZoomControls = () => {
const maskEditorRef = React.useRef(null);
return (
<>
>
);
};
`
---
react-canvas-masker is great for:
- โจ AI image editing apps (e.g. Stable Diffusion, DALLยทE, Sora, etc.)
- ๐ง Web-based design tools (like Figma clones or mockup tools)
- ๐ Educational tools where users interact with images
- ๐ฎ Selective filtering or redacting images (blur, crop, etc.)
- ๐ Creative playgrounds or generative UIs
---
- All mask operations are done on a separate canvas for performance
- The mask is returned as a black-and-white PNG (base64)
- Supports up to 50 undo/redo steps
- Forked and modernized from react-mask-editor`
---
MIT
---
This is a cleaned-up and improved version of an unmaintained package, refactored into a hook-first, React 18+ friendly library with a focus on AI tooling and performance. Key enhancements include:
- Advanced zoom and pan capabilities for precise editing
- Optimized event handling and rendering
- Responsive design that adapts to container dimensions
- Improved coordinate calculations for pixel-perfect precision
- Enhanced user controls with intuitive keyboard and mouse interactions