@savio99/react-draw
v1.2.6
Published
Simple responsive draw component to sign and draw in your own website
Maintainers
Readme
@savio99/react-draw
A powerful React whiteboard/drawing library with support for freehand drawing, images, pan & zoom, grid backgrounds, floating toolbox, and more.
Features
- ✏️ Freehand Drawing - Smooth pen strokes with customizable color and width
- 🧽 Eraser Tool - Erase strokes with configurable eraser size
- 🖼️ Image Support - Add, move, resize, and rotate images
- 🔍 Pan & Zoom - Navigate large canvases with mouse wheel, touch gestures, or middle-click
- 📐 Grid Background - Optional customizable grid overlay
- 🧰 Floating Toolbox - Draggable toolbar with buttons, sliders, color pickers, and file inputs
- 📺 Fullscreen Mode - Expand whiteboard to fullscreen
- 💾 Export/Import JSON - Save and load whiteboard state
- 🖼️ Export to Image - Download whiteboard as PNG/JPEG with custom resolution
- 🔄 Auto-fit Preview - Automatically scale content to fit container
- ↩️ Undo Support - Undo last stroke
- ✋ Hand Mode - Pan canvas by dragging without holding special keys
- 🖊️ Stylus Support - Full support for Samsung S-Pen, Apple Pencil, and other styluses
- 👆 Touch Support - Multi-touch gestures for pinch-to-zoom and two-finger pan
- 🎯 Pen Only Mode - Ignore finger touch, use only stylus input (iOS/Android)
- 📏 Dimensions - Add measurement annotations with customizable values
Installation
npm install --save @savio99/react-draw
# or
yarn add @savio99/react-drawQuick Start
Option 1: Use DrawingBoard (Complete Solution)
The easiest way to get started. DrawingBoard is a ready-to-use component with all features pre-configured:
import { DrawingBoard } from '@savio99/react-draw';
function App() {
return (
<div style={{ height: '100vh' }}>
<DrawingBoard
showGrid={true}
onChangeStrokes={(strokes) => console.log('Strokes changed:', strokes.length)}
/>
</div>
);
}Option 2: Use Whiteboard (Custom Solution)
For full control, use the Whiteboard component directly:
import { useRef, useState } from 'react';
import Whiteboard, { Stroke } from '@savio99/react-draw';
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [strokes, setStrokes] = useState<Stroke[]>([]);
return (
<Whiteboard
ref={whiteboard}
containerStyle={{
style: {
border: '2px solid black',
borderRadius: 10,
height: '80vh'
}
}}
onChangeStrokes={(strokes) => setStrokes(strokes || [])}
/>
);
}DrawingBoard Component
A complete, ready-to-use drawing board with all features enabled. Includes a floating toolbox, mode switching, export/import, and more.
DrawingBoard Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| initialStrokes | Stroke[] | [] | Initial strokes to display |
| initialImages | SketchImage[] | [] | Initial images to display |
| initialDimensions | DimensionData[] | [] | Initial dimensions to display |
| showGrid | boolean | true | Whether to show the grid |
| gridSize | number | 25 | Grid size in pixels |
| gridColor | string | '#e0e0e0' | Grid line color |
| gridOpacity | number | 0.8 | Grid line opacity (0-1) |
| minZoom | number | 0.25 | Minimum zoom level |
| maxZoom | number | 4 | Maximum zoom level |
| dimensionColor | string | '#ff5722' | Color for dimension lines |
| defaultPenColor | string | '#000000' | Default pen color |
| defaultPenWidth | number | 4 | Default pen width |
| style | CSSProperties | - | Container style |
| colorPalette | string[] | (10 colors) | Color palette for the picker |
| toolboxPosition | {x, y} | {x:20, y:20} | Toolbox initial position |
| toolboxOrientation | 'horizontal' \| 'vertical' | 'vertical' | Toolbox orientation |
| additionalActions | ToolboxAction[] | [] | Custom actions to add |
| hideActions | string[] | [] | Hide default actions by id |
| labels | object | (Italian) | Labels for UI elements (i18n) |
| onChangeStrokes | function | - | Callback when strokes change |
| onChangeImages | function | - | Callback when images change |
| onChangeDimensions | function | - | Callback when dimensions change |
| onFullscreenChange | function | - | Callback when fullscreen changes |
DrawingBoard Ref Methods
const boardRef = useRef<DrawingBoardRef>(null);
// Export data
const data = boardRef.current?.exportData();
const json = boardRef.current?.exportJSON();
// Import data
boardRef.current?.importData(data);
boardRef.current?.importJSON(jsonString);
// Download as image
await boardRef.current?.downloadImage('my-drawing', { format: 'png', scale: 2 });
// Other actions
boardRef.current?.clear();
boardRef.current?.undo();
boardRef.current?.resetView();
boardRef.current?.toggleFullscreen();
// Access underlying Whiteboard
const whiteboard = boardRef.current?.getWhiteboard();Customizing Labels (i18n)
<DrawingBoard
labels={{
pen: 'Pen',
hand: 'Pan',
dimension: 'Measure',
select: 'Select',
eraser: 'Eraser',
penOnly: 'Stylus Only',
color: 'Pen Color',
strokeWidth: 'Pen Size',
addImage: 'Add Image',
undo: 'Undo',
clear: 'Clear All',
grid: 'Toggle Grid',
fullscreen: 'Fullscreen',
resetView: 'Reset View',
modeLabel: 'Mode',
penOnlyActive: 'Stylus Only active',
instructions: 'Ctrl + scroll to zoom, middle-click or two fingers to pan'
}}
/>API Reference
Whiteboard Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | true | Enable/disable drawing. Set to false for mouse mode (select/move images) |
| containerStyle | object | - | Container div props including style |
| strokeColor | string | '#000000' | Initial stroke color |
| strokeWidth | number | 4 | Initial stroke width |
| strokes | Stroke[] | - | Controlled strokes array |
| initialStrokes | Stroke[] | [] | Initial strokes (uncontrolled) |
| onChangeStrokes | (strokes?: Stroke[]) => void | - | Callback when strokes change |
| images | SketchImage[] | - | Controlled images array |
| initialImages | SketchImage[] | [] | Initial images (uncontrolled) |
| onChangeImages | (images: SketchImage[]) => void | - | Callback when images change |
| showGrid | boolean | false | Show grid background |
| gridSize | number | 20 | Grid cell size in pixels |
| gridColor | string | '#cccccc' | Grid line color |
| gridOpacity | number | 0.5 | Grid line opacity (0-1) |
| enablePan | boolean | false | Enable panning (middle-click or two-finger touch) |
| enableZoom | boolean | false | Enable zooming (Ctrl+scroll or pinch) |
| minZoom | number | 0.5 | Minimum zoom level |
| maxZoom | number | 3 | Maximum zoom level |
| onFullscreenChange | (isFullscreen: boolean) => void | - | Callback when fullscreen state changes |
| autoFit | boolean | false | Auto-fit content to container (useful for preview) |
| autoFitPadding | number | 20 | Padding around content when auto-fitting |
| eraserWidth | number | 20 | Eraser width in pixels |
| children | ReactNode | - | Children (e.g., FloatingToolbox) |
| mode | 'pen' \| 'hand' \| 'dimension' \| 'mouse' \| 'eraser' | 'pen' | Current interaction mode |
| onModeChange | (mode: WhiteboardMode) => void | - | Callback when mode changes |
| penOnly | boolean | false | Only accept stylus input, ignore finger touch (iOS/Android) |
| onPenOnlyChange | (penOnly: boolean) => void | - | Callback when penOnly changes |
| dimensions | DimensionData[] | - | Controlled dimensions array |
| initialDimensions | DimensionData[] | [] | Initial dimensions (uncontrolled) |
| onChangeDimensions | (dimensions: DimensionData[]) => void | - | Callback when dimensions change |
| dimensionColor | string | '#ff5722' | Default color for new dimensions |
Whiteboard Methods
Access methods via ref:
const whiteboard = useRef<Whiteboard>(null);
// Then use:
whiteboard.current?.methodName();Drawing Methods
| Method | Description |
|--------|-------------|
| undo() | Undo the last stroke |
| clear() | Clear all strokes and images |
| clearStrokes() | Clear only strokes |
| clearImages() | Clear only images |
| changeColor(color: string) | Change stroke color |
| changeStrokeWidth(width: number) | Change stroke width |
Image Methods
| Method | Description |
|--------|-------------|
| addImage(src, x?, y?, width?, height?) | Add image to whiteboard |
| removeImage(imageId: string) | Remove image by ID |
| updateImage(imageId, updates) | Update image properties |
| selectImage(imageId: string \| null) | Select/deselect image |
| getImages() | Get all images |
Mode Methods
| Method | Description |
|--------|-------------|
| setMode(mode: WhiteboardMode) | Set interaction mode ('pen', 'hand', 'dimension', 'mouse', 'eraser') |
| getMode() | Get current interaction mode |
| setPenOnly(enabled: boolean) | Enable/disable stylus-only mode |
| getPenOnly() | Check if penOnly mode is enabled |
Dimension Methods
| Method | Description |
|--------|-------------|
| addDimension(startX, startY, endX, endY, value?) | Add dimension line |
| removeDimension(dimensionId: string) | Remove dimension by ID |
| updateDimension(dimensionId, updates) | Update dimension properties |
| selectDimension(dimensionId: string \| null) | Select/deselect dimension |
| getDimensions() | Get all dimensions |
| clearDimensions() | Clear all dimensions |
| editDimensionValue(dimensionId: string) | Open modal to edit dimension value |
View Methods
| Method | Description |
|--------|-------------|
| setPan(x: number, y: number) | Set pan position |
| setZoom(scale: number) | Set zoom level |
| resetView() | Reset pan and zoom to defaults |
| fitToContent(padding?: number) | Fit view to show all content |
| toggleFullscreen() | Toggle fullscreen mode |
| isFullscreen() | Check if in fullscreen mode |
| getBounds() | Get bounding box of all content |
Export/Import Methods
| Method | Description |
|--------|-------------|
| exportData() | Export as WhiteboardData object |
| exportJSON() | Export as JSON string |
| importData(data, options?) | Import from WhiteboardData object |
| importJSON(json, options?) | Import from JSON string |
| exportToImage(options?) | Export as data URL |
| downloadImage(filename?, options?) | Download as image file |
Types
Stroke
interface Stroke {
box: { width: number; height: number };
points: Point[];
color: string;
width: number;
}SketchImage
interface SketchImage {
id: string;
src: string;
x: number;
y: number;
width: number;
height: number;
rotation?: number;
opacity?: number;
}DimensionData
interface DimensionData {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
value: string;
color?: string;
fontSize?: number;
lineWidth?: number;
}WhiteboardMode
type WhiteboardMode = 'pen' | 'hand' | 'dimension' | 'mouse' | 'eraser';WhiteboardData
interface WhiteboardData {
version: string;
strokes: Stroke[];
images: SketchImage[];
viewState?: {
panX: number;
panY: number;
scale: number;
};
}Export Image Options
interface ExportImageOptions {
format?: 'png' | 'jpeg';
quality?: number; // 0-1, for JPEG
width?: number; // Output width
height?: number; // Output height
scale?: number; // Scale factor (e.g., 2 for 2x resolution)
backgroundColor?: string;
}Examples
Basic Drawing with Controls
import { useRef, useState } from 'react';
import Whiteboard, { Stroke } from '@savio99/react-draw';
function DrawingApp() {
const whiteboard = useRef<Whiteboard>(null);
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<button onClick={() => whiteboard.current?.undo()}>Undo</button>
<button onClick={() => whiteboard.current?.clear()}>Clear</button>
<input
type="color"
onChange={(e) => whiteboard.current?.changeColor(e.target.value)}
/>
<input
type="range"
min={1}
max={50}
defaultValue={4}
onChange={(e) => whiteboard.current?.changeStrokeWidth(parseInt(e.target.value))}
/>
</div>
<Whiteboard
ref={whiteboard}
containerStyle={{ style: { height: '500px', border: '1px solid #ccc' } }}
/>
</div>
);
}With Grid and Pan/Zoom
<Whiteboard
ref={whiteboard}
showGrid={true}
gridSize={25}
gridColor="#e0e0e0"
gridOpacity={0.8}
enablePan={true}
enableZoom={true}
minZoom={0.25}
maxZoom={4}
containerStyle={{ style: { height: '100vh' } }}
/>With FloatingToolbox
import Whiteboard, { FloatingToolbox, ToolboxAction } from '@savio99/react-draw';
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [color, setColor] = useState('#000000');
const [strokeWidth, setStrokeWidth] = useState(4);
const actions: ToolboxAction[] = [
{
id: 'color',
label: 'Color',
type: 'colorPicker',
value: color,
onChange: (value) => {
setColor(value as string);
whiteboard.current?.changeColor(value as string);
},
colors: ['#000000', '#ff0000', '#00ff00', '#0000ff', '#ffff00']
},
{
id: 'size',
label: 'Size',
type: 'slider',
value: strokeWidth,
min: 1,
max: 50,
onChange: (value) => {
setStrokeWidth(value as number);
whiteboard.current?.changeStrokeWidth(value as number);
}
},
{
id: 'addImage',
label: 'Add Image',
type: 'file',
accept: 'image/*',
onChange: (src) => whiteboard.current?.addImage(src as string),
icon: <span>🖼️</span>
},
{
id: 'undo',
label: 'Undo',
onClick: () => whiteboard.current?.undo(),
icon: <span>↩️</span>
},
{
id: 'clear',
label: 'Clear',
onClick: () => whiteboard.current?.clear(),
icon: <span>🗑️</span>
}
];
return (
<Whiteboard ref={whiteboard} containerStyle={{ style: { height: '100vh' } }}>
<FloatingToolbox
actions={actions}
initialPosition={{ x: 20, y: 20 }}
orientation="vertical"
containerRef={whiteboard.current?.getContainerRef()}
/>
</Whiteboard>
);
}Mouse Mode for Image Selection
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [mouseMode, setMouseMode] = useState(false);
return (
<div>
<button onClick={() => setMouseMode(!mouseMode)}>
{mouseMode ? 'Drawing Mode' : 'Mouse Mode'}
</button>
<Whiteboard
ref={whiteboard}
enabled={!mouseMode} // Disable drawing in mouse mode
containerStyle={{ style: { height: '500px' } }}
/>
</div>
);
}Mirror Preview with Auto-Fit
function App() {
const [strokes, setStrokes] = useState<Stroke[]>([]);
const [images, setImages] = useState<SketchImage[]>([]);
return (
<div>
{/* Main whiteboard */}
<Whiteboard
onChangeStrokes={(s) => setStrokes(s || [])}
onChangeImages={(i) => setImages(i)}
containerStyle={{ style: { height: '60vh' } }}
/>
{/* Mirror preview - auto-fits content */}
<Whiteboard
strokes={strokes}
images={images}
enabled={false}
autoFit={true}
autoFitPadding={30}
containerStyle={{
style: {
height: 200,
width: '50%',
margin: '0 auto',
border: '1px solid #ccc'
}
}}
/>
</div>
);
}Export/Import JSON
function App() {
const whiteboard = useRef<Whiteboard>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleExport = () => {
const data = whiteboard.current?.exportData();
if (!data) return;
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'whiteboard.json';
a.click();
URL.revokeObjectURL(url);
};
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const json = event.target?.result as string;
whiteboard.current?.importJSON(json);
};
reader.readAsText(file);
};
return (
<div>
<button onClick={handleExport}>Export</button>
<button onClick={() => fileInputRef.current?.click()}>Import</button>
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleImport}
style={{ display: 'none' }}
/>
<Whiteboard ref={whiteboard} containerStyle={{ style: { height: '500px' } }} />
</div>
);
}Export to Image
function App() {
const whiteboard = useRef<Whiteboard>(null);
const handleExportPNG = async () => {
await whiteboard.current?.downloadImage('my-drawing.png', {
format: 'png',
scale: 2, // 2x resolution
backgroundColor: '#ffffff'
});
};
const handleExportJPEG = async () => {
await whiteboard.current?.downloadImage('my-drawing.jpg', {
format: 'jpeg',
quality: 0.9,
width: 1920, // Fixed width
backgroundColor: '#ffffff'
});
};
const handleGetDataUrl = async () => {
const dataUrl = await whiteboard.current?.exportToImage({
format: 'png',
scale: 1
});
console.log(dataUrl); // Can be used as img src
};
return (
<div>
<button onClick={handleExportPNG}>Export PNG (2x)</button>
<button onClick={handleExportJPEG}>Export JPEG (1920px)</button>
<button onClick={handleGetDataUrl}>Get Data URL</button>
<Whiteboard ref={whiteboard} containerStyle={{ style: { height: '500px' } }} />
</div>
);
}Fullscreen Mode
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
return (
<div>
<button onClick={() => whiteboard.current?.toggleFullscreen()}>
{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
</button>
<Whiteboard
ref={whiteboard}
onFullscreenChange={setIsFullscreen}
containerStyle={{
style: {
height: '500px',
backgroundColor: '#ffffff' // Background in fullscreen
}
}}
/>
</div>
);
}Mode Switching (Pen, Hand, Dimension, Mouse, Eraser)
import { useRef, useState } from 'react';
import Whiteboard, { WhiteboardMode, FloatingToolbox, ToolboxAction } from '@savio99/react-draw';
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [mode, setMode] = useState<WhiteboardMode>('pen');
const [penOnly, setPenOnly] = useState(false);
const actions: ToolboxAction[] = [
{
id: 'pen',
label: 'Pen',
active: mode === 'pen',
onClick: () => {
setMode('pen');
whiteboard.current?.setMode('pen');
},
icon: <span>✏️</span>
},
{
id: 'hand',
label: 'Hand (Pan)',
active: mode === 'hand',
onClick: () => {
setMode('hand');
whiteboard.current?.setMode('hand');
},
icon: <span>✋</span>
},
{
id: 'dimension',
label: 'Dimension',
active: mode === 'dimension',
onClick: () => {
setMode('dimension');
whiteboard.current?.setMode('dimension');
},
icon: <span>📏</span>
},
{
id: 'mouse',
label: 'Select',
active: mode === 'mouse',
onClick: () => {
setMode('mouse');
whiteboard.current?.setMode('mouse');
},
icon: <span>🖱️</span>
},
{
id: 'eraser',
label: 'Eraser',
active: mode === 'eraser',
onClick: () => {
setMode('eraser');
whiteboard.current?.setMode('eraser');
},
icon: <span>🧽</span>
},
{
id: 'penOnly',
label: 'Stylus Only',
active: penOnly,
onClick: () => {
const newValue = !penOnly;
setPenOnly(newValue);
whiteboard.current?.setPenOnly(newValue);
},
icon: <span>🖊️</span>
}
];
return (
<Whiteboard
ref={whiteboard}
mode={mode}
penOnly={penOnly}
enablePan={true}
enableZoom={true}
containerStyle={{ style: { height: '100vh' } }}
>
<FloatingToolbox
actions={actions}
initialPosition={{ x: 20, y: 20 }}
orientation="vertical"
/>
</Whiteboard>
);
}Working with Dimensions
import { useRef, useState } from 'react';
import Whiteboard, { DimensionData } from '@savio99/react-draw';
function App() {
const whiteboard = useRef<Whiteboard>(null);
const [dimensions, setDimensions] = useState<DimensionData[]>([]);
return (
<div>
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
<button onClick={() => whiteboard.current?.setMode('dimension')}>
Add Dimension
</button>
<button onClick={() => whiteboard.current?.setMode('mouse')}>
Select Mode
</button>
<button onClick={() => whiteboard.current?.clearDimensions()}>
Clear Dimensions
</button>
</div>
<Whiteboard
ref={whiteboard}
enablePan={true}
enableZoom={true}
onChangeDimensions={setDimensions}
dimensionColor="#ff5722"
containerStyle={{ style: { height: '500px', border: '1px solid #ccc' } }}
/>
<div>
<h4>Dimensions:</h4>
<ul>
{dimensions.map(d => (
<li key={d.id}>{d.value || 'No value'}</li>
))}
</ul>
</div>
</div>
);
}FloatingToolbox
A draggable toolbar component that can be placed inside the Whiteboard.
FloatingToolbox Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| actions | ToolboxAction[] | - | Array of toolbar actions |
| visible | boolean | true | Show/hide toolbox |
| initialPosition | { x: number; y: number } | { x: 20, y: 20 } | Initial position |
| orientation | 'horizontal' \| 'vertical' | 'vertical' | Layout direction |
| style | CSSProperties | - | Additional styles |
| containerRef | RefObject<HTMLElement> | - | Reference to whiteboard container for bounds |
ToolboxAction Types
interface ToolboxAction {
id: string;
label: string;
icon?: ReactNode;
onClick?: () => void;
active?: boolean;
type?: 'button' | 'color' | 'number' | 'file' | 'slider' | 'colorPicker';
value?: string | number | boolean;
onChange?: (value: string | number | boolean) => void;
min?: number; // For slider
max?: number; // For slider
accept?: string; // For file input
colors?: string[]; // For colorPicker
}Action Types
- button (default): Simple click button
- colorPicker: Color grid popup with presets
- slider: Range slider with popup
- file: File input (returns data URL for images)
- color: Native color input
- number: Native number input
Keyboard, Mouse & Touch Controls
| Action | Control |
|--------|---------|
| Zoom | Ctrl + Scroll, pinch gesture (two fingers), or mouse wheel |
| Pan | Middle mouse button drag, two-finger touch, or Hand mode |
| Draw | Left mouse button, stylus, or single-finger touch (in Pen mode) |
| Erase | Left mouse button, stylus, or touch drag (in Eraser mode) |
| Select Image | Click/tap image (in Mouse mode or when enabled={false}) |
| Move Image | Drag selected image with mouse, stylus, or touch |
| Resize Image | Drag corner handles with mouse, stylus, or touch |
| Add Dimension | Click/tap and drag (in Dimension mode) |
| Edit Dimension | Double-click/tap dimension (in Mouse mode) |
| Select Dimension | Click/tap dimension (in Mouse mode) |
| Move Dimension | Drag dimension body (in Mouse mode) |
| Resize Dimension | Drag endpoint handles (in Mouse mode) |
Interaction Modes
| Mode | Description |
|------|-------------|
| pen | Default drawing mode. Left-click/touch/stylus draws strokes |
| hand | Pan mode. Left-click/touch/stylus pans the canvas |
| dimension | Dimension mode. Click/tap and drag to add measurement lines |
| mouse | Selection mode. Click/tap to select images, dimensions, or strokes |
| eraser | Eraser mode. Drag to erase strokes that intersect with the eraser |
Stylus & Touch Support
The library provides full support for stylus input (Samsung S-Pen, Apple Pencil, Surface Pen, etc.) and touch gestures:
- Stylus Detection: Automatically detects pen vs touch vs mouse input
- Pressure Sensitivity: Works with pressure-sensitive styluses
- Palm Rejection: When
penOnlyis enabled, finger touches are ignored for drawing but can still be used for panning - Multi-touch Gestures: Two-finger pinch-to-zoom and pan work in all modes
- Touch-friendly UI: All toolbox buttons and controls work with touch input
When penOnly is enabled, the whiteboard ignores regular finger touches for drawing and only responds to stylus/pen input. Finger touches will pan the canvas instead, providing a natural drawing experience on tablets.
License
MIT © savio-99
