🗾 Simple React whiteboard sketching hook
npm install react-whiteboard-sketchA simple, fully-typed React hook for adding whiteboard/canvas drawing functionality to your React applications.
- Simple Hook-Based API - No components required, just use the hook
- Drawing with Strokes - Draw with customizable colors and widths
- Eraser Tool - Erase parts of your drawing with adjustable width
- Background Images - Add images as canvas backgrounds with aspect ratio control
- PNG Export - Export your drawings as PNG (including background)
- State Persistence - Save and load whiteboard state (localStorage, API, etc.)
- Read-Only Mode - Display completed drawings without editing
- Undo/Redo - Full history support for going back and forward
- Fully Typed - Written in TypeScript with complete type definitions
- Cross-Browser Compatible - Works with mouse and touch events
- Reactive - All changes trigger automatic re-renders
``bash`
npm install react-whiteboard-sketch
`bash`
yarn add react-whiteboard-sketch
`bash`
pnpm add react-whiteboard-sketch
`tsx
import { useWhiteboard, Canvas } from 'react-whiteboard-sketch';
function App() {
const { canvasRef, clear, undo, redo, exportAsPNG } = useWhiteboard({
width: 800,
height: 600,
strokeColor: '#000000',
strokeWidth: 2,
backgroundColor: '#ffffff', // Default white background
});
const handleExport = () => {
const dataUrl = exportAsPNG();
// Exports with white background, not transparent
const link = document.createElement('a');
link.download = 'whiteboard.png';
link.href = dataUrl;
link.click();
};
return (
> Note: The canvas is automatically optimized for high-DPI displays (Retina, etc.) for sharp rendering.
API Reference
$3
The main hook for creating a whiteboard instance.
#### Options
`typescript
interface WhiteboardOptions {
/**
* Stroke color (CSS color string)
* @default "#000000"
*/
strokeColor?: string; /**
* Stroke width in pixels
* @default 2
*/
strokeWidth?: number;
/**
* Background image URL or HTMLImageElement
*/
backgroundImage?: string | HTMLImageElement | null;
/**
* Canvas width in pixels
* @default 800
*/
width?: number;
/**
* Canvas height in pixels
* @default 600
*/
height?: number;
}
`#### Returns
`typescript
interface WhiteboardResult {
/**
* Ref to attach to the canvas element
*/
canvasRef: RefObject; /**
* Clear all strokes from the canvas
*/
clear: () => void;
/**
* Undo the last stroke
*/
undo: () => void;
/**
* Redo the previously undone stroke
*/
redo: () => void;
/**
* Export the canvas as PNG data URL
*/
exportAsPNG: () => string;
/**
* Set the stroke color
*/
setStrokeColor: (color: string) => void;
/**
* Set the stroke width
*/
setStrokeWidth: (width: number) => void;
/**
* Set the background image
*/
setBackgroundImage: (image: string | HTMLImageElement | null) => void;
/**
* Check if undo is available
*/
canUndo: boolean;
/**
* Check if redo is available
*/
canRedo: boolean;
}
`Advanced Examples
$3
`tsx
import { useState } from 'react';
import { useWhiteboard } from 'react-whiteboard-sketch';function WhiteboardApp() {
const [color, setColor] = useState('#000000');
const [width, setWidth] = useState(5);
const {
canvasRef,
clear,
undo,
redo,
setStrokeColor,
setStrokeWidth,
canUndo,
canRedo,
} = useWhiteboard({
width: 1000,
height: 700,
strokeColor: color,
strokeWidth: width,
});
const handleColorChange = (newColor: string) => {
setColor(newColor);
setStrokeColor(newColor);
};
const handleWidthChange = (newWidth: number) => {
setWidth(newWidth);
setStrokeWidth(newWidth);
};
return (
type="color"
value={color}
onChange={(e) => handleColorChange(e.target.value)}
/>
type="range"
min="1"
max="50"
value={width}
onChange={(e) => handleWidthChange(Number(e.target.value))}
/>
ref={canvasRef}
style={{
border: '1px solid #ccc',
cursor: 'crosshair',
touchAction: 'none',
}}
/>
);
}
`$3
`tsx
import { useWhiteboard, Canvas } from 'react-whiteboard-sketch';function WhiteboardWithBackground() {
const {
canvasRef,
setBackgroundImage,
exportAsPNG,
} = useWhiteboard({
width: 800,
height: 600,
backgroundImage: 'https://example.com/background.jpg',
// Background images always preserve aspect ratio (default: 'contain')
preserveBackgroundImageAspectRatio: 'contain', // or 'cover', 'fill', 'none', 'scale-down'
});
const handleExport = () => {
const dataUrl = exportAsPNG();
// The exported PNG includes the background image
const link = document.createElement('a');
link.download = 'whiteboard-with-background.png';
link.href = dataUrl;
link.click();
};
return (
type="text"
placeholder="Background URL"
onChange={(e) => setBackgroundImage(e.target.value)}
/>
);
}
`$3
`tsx
import { useWhiteboard, Canvas } from 'react-whiteboard-sketch';function TransparentWhiteboard() {
const { canvasRef, exportAsPNG } = useWhiteboard({
width: 800,
height: 600,
backgroundColor: 'transparent', // For transparent PNG export
});
return ;
}
`$3
`tsx
import { useWhiteboard } from 'react-whiteboard-sketch';function WhiteboardWithEraser() {
const {
canvasRef,
mode,
setMode,
eraserWidth,
setEraserWidth,
} = useWhiteboard({
width: 800,
height: 600,
eraserWidth: 20,
});
return (
Mode: {mode} {mode === 'erase' && (
type="range"
min="5"
max="100"
value={eraserWidth}
onChange={(e) => setEraserWidth(Number(e.target.value))}
/>
)}
);
}
`$3
`tsx
import { useState, useEffect } from 'react';
import { useWhiteboard, type WhiteboardState } from 'react-whiteboard-sketch';function PersistentWhiteboard() {
const STORAGE_KEY = 'my-whiteboard';
// Load initial state from localStorage
const [initialState] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : undefined;
});
const {
canvasRef,
getState,
loadState,
clear,
} = useWhiteboard({
width: 800,
height: 600,
initialState,
});
// Auto-save every second
useEffect(() => {
const interval = setInterval(() => {
const state = getState();
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}, 1000);
return () => clearInterval(interval);
}, [getState]);
const handleClear = () => {
clear();
localStorage.removeItem(STORAGE_KEY);
};
return (
);
}
`$3
`tsx
import { useWhiteboard, type WhiteboardState } from 'react-whiteboard-sketch';function APIWhiteboard() {
const { canvasRef, getState, loadState } = useWhiteboard({
width: 800,
height: 600,
});
const handleSave = async () => {
const state = getState();
await fetch('/api/whiteboard/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state),
});
};
const handleLoad = async () => {
const response = await fetch('/api/whiteboard/load');
const state: WhiteboardState = await response.json();
loadState(state);
};
return (
);
}
`$3
`tsx
import { useWhiteboard, type WhiteboardState } from 'react-whiteboard-sketch';function ReadOnlyWhiteboard({ savedState }: { savedState: WhiteboardState }) {
const { canvasRef } = useWhiteboard({
width: 800,
height: 600,
initialState: savedState,
readOnly: true, // Disable all drawing interactions
});
return (
This whiteboard is in read-only mode
);
}
`TypeScript
This package is written in TypeScript and includes all type definitions. No need for additional
@types packages.`typescript
import type {
WhiteboardOptions,
WhiteboardResult,
WhiteboardState,
Stroke,
StrokePoint,
DrawingMode,
} from 'react-whiteboard-sketch';
``Works in all modern browsers that support:
- HTML5 Canvas
- React 19+
- ES6+
Tested on:
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
MIT © Conner Bachmann
Contributions are welcome! Please feel free to submit a Pull Request.