Install
openclaw skills install image-annotation-usageUse when integrating @frank17008/image-annotation React component into web applications, including basic setup, controlled/uncontrolled mode, custom image upload, styling, or debugging annotation rendering issues, canvas blank problems, or annotations not updating.
openclaw skills install image-annotation-usage@frank17008/image-annotation is a React + TypeScript image annotation component with Canvas 2D. Supports rectangle, circle, arrow, text, and freehand tools with undo/redo, drag, export, keyboard shortcuts, zoom/pan, and DPR-aware rendering.
Package location: packages/image-annotation/src/
Published path: @frank17008/image-annotation
Trigger phrases:
pnpm add @frank17008/image-annotation
# or
npm install @frank17008/image-annotation
import { useState } from 'react';
import { ImageAnnotation, type Annotation } from '@frank17008/image-annotation';
import '@frank17008/image-annotation/dist/index.css';
export default function App() {
const [annotations, setAnnotations] = useState<Annotation[]>([]);
return (
<div style={{ height: 600 }}>
<ImageAnnotation
src="https://example.com/image.jpg"
onChange={setAnnotations}
/>
</div>
);
}
Critical: Parent container MUST have explicit height. Without it, canvas has 0×0 dimensions → blank canvas.
Always use explicit type import for TypeScript:
import type { Annotation, ToolType } from '@frank17008/image-annotation';
// or
import { type Annotation, type ToolType } from '@frank17008/image-annotation';
<ImageAnnotation
src="image.jpg"
onChange={(annotations) => console.log(annotations)}
/>
The component manages its own state internally.
const [myAnnotations, setMyAnnotations] = useState<Annotation[]>([]);
<ImageAnnotation
src="image.jpg"
value={myAnnotations}
onChange={setMyAnnotations}
/>
Rules:
value → external annotations (read-only for component)onChange → called when annotations changevalue and onChange provided → controlled modevalue without onChange → React warning<ImageAnnotation
src={src}
value={annotations}
onChange={(newAnnotations) => {
setAnnotations(newAnnotations);
// Save to backend, localStorage, etc.
saveToBackend(newAnnotations);
}}
/>
import { useState, useRef } from 'react';
import { ImageAnnotation } from '@frank17008/image-annotation';
export function ImageUploader() {
const [imageSrc, setImageSrc] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === 'string') {
setImageSrc(result); // base64 string
}
};
reader.readAsDataURL(file);
e.target.value = ''; // reset for same file re-select
};
return (
<div style={{ height: 600 }}>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<button onClick={() => fileInputRef.current?.click()}>
Upload Image
</button>
{imageSrc && (
<ImageAnnotation
src={imageSrc}
onChange={() => {}}
/>
)}
</div>
);
}
const handleUpload = () => fileInputRef.current?.click();
<ImageAnnotation
src={imageSrc}
onChange={setAnnotations}
onUpload={handleUpload}
/>
The component displays an upload button in toolbar when onUpload is provided.
// Rectangle annotation
{ type: 'rectangle', x: number, y: number, width: number, height: number, color: string, lineWidth?: number }
// Circle annotation
{ type: 'circle', x: number, y: number, radius: number, color: string, lineWidth?: number }
// Arrow annotation
{ type: 'arrow', x: number, y: number, toX: number, toY: number, color: string, lineWidth?: number }
// Text annotation
{ type: 'text', x: number, y: number, text: string, color: string, lineWidth?: number }
// Freehand annotation
{ type: 'freehand', points: Array<{ x: number, y: number }>, color: string, lineWidth?: number }
import type {
Annotation,
RectangleAnnotation,
CircleAnnotation,
ArrowAnnotation,
TextAnnotation,
FreehandAnnotation,
ToolType,
Point,
} from '@frank17008/image-annotation';
| Prop | Type | Required | Description |
|---|---|---|---|
src | string | ✅ | Image URL or base64 |
value | Annotation[] | ❌ | Controlled annotations |
onChange | (annotations: Annotation[]) => void | ❌ | Annotation change callback |
className | string | ❌ | Container className |
onUpload | () => void | ❌ | Upload button callback |
const handleZoomIn = () => {
// Component doesn't expose zoom API directly
// Use keyboard shortcuts: Ctrl++
};
// Or via keyboard shortcuts (user must press):
// - Ctrl++ : Zoom in
// - Ctrl+- : Zoom out
// - Ctrl+0 : Reset zoom
Hold Space + drag to pan the canvas.
The toolbar shows current zoom percentage (e.g., "100%", "150%").
<ImageAnnotation
src={src}
onChange={(annotations) => {
// Save to database
await saveAnnotations(imageId, annotations);
// Or export as JSON
const json = JSON.stringify(annotations, null, 2);
download(json, 'annotations.json', 'application/json');
}}
/>
Use the toolbar's export button, or programmatically:
import { download } from '@frank17008/image-annotation';
// Not currently exported - use toolbar button
| Shortcut | Action |
|---|---|
| Delete | Delete selected annotation |
| Escape | Cancel text input |
| Ctrl+Z | Undo |
| Ctrl+Y / Ctrl+Shift+Z | Redo |
| Ctrl++ / Ctrl+= | Zoom in |
| Ctrl+- | Zoom out |
| Ctrl+0 | Reset zoom |
| Space + Drag | Pan canvas |
:root {
--annotation-primary: #FF0000;
--annotation-toolbar-bg: #FFFFFF;
}
<ImageAnnotation
src={src}
className="my-annotation-container"
/>
.my-annotation-container {
border: 1px solid #ddd;
border-radius: 8px;
}
Symptoms:
Root Causes and Solutions:
| Cause | Diagnosis | Fix |
|---|---|---|
| Parent has no height | Container has height: auto or no height set | Set explicit height: style={{ height: 600 }} |
| DPR mismatch | Retina/HiDPI display, canvas looks tiny | Component handles DPR automatically |
| Image load error | Check browser console for CORS errors | Add crossOrigin="anonymous" to server |
| Zero canvas size | Check canvas element dimensions | Ensure parent has explicit dimensions |
Symptoms:
onChange callback never firesRoot Causes and Solutions:
| Cause | Diagnosis | Fix |
|---|---|---|
| Stale state | Using prev => prev incorrectly | Pass new array directly: setAnnotations(newValue) |
| Missing onChange | No callback provided | Add onChange={setAnnotations} |
| Controlled conflict | Both value and internal state | Provide onChange with value |
Correct pattern:
// WRONG - stale closure
onChange={(annotations) => {
setAnnotations((prev) => [...prev, ...annotations]);
}}
// CORRECT - direct assignment
onChange={setAnnotations}
Diagnosis:
Root Cause: Missing devicePixelRatio scaling
Fix: The component handles DPR automatically since v1.x. Ensure:
Symptoms:
Root Cause: Canvas dimensions not scaled by devicePixelRatio
Diagnosis:
// In browser console
const canvas = document.querySelector('canvas');
console.log('Canvas size:', canvas.width, canvas.height);
console.log('Display size:', canvas.style.width, canvas.style.height);
console.log('DPR:', window.devicePixelRatio);
If canvas.width === canvas.style.width (not multiplied by DPR), this is the bug.
Symptoms:
Solution: Press Enter or click outside to commit text input.
Symptoms:
Root Cause: No history to undo (first annotation can't be undone)
Symptoms:
Solution:
'use client';
import dynamic from 'next/dynamic';
const ImageAnnotation = dynamic(
() => import('@frank17008/image-annotation').then((mod) => mod.default),
{ ssr: false }
);
pnpm build:lib # Build package to dist/
pnpm dev:lib # Watch mode for library
pnpm start # Run demo app
pnpm build:all # Build everything
// MISSING - component won't render correctly
import { ImageAnnotation } from '@frank17008/image-annotation';
// REQUIRED
import '@frank17008/image-annotation/dist/index.css';
// WRONG - canvas has 0x0
<div><ImageAnnotation src="..." /></div>
// CORRECT - explicit height
<div style={{ height: 600 }}><ImageAnnotation src="..." /></div>
// WRONG
import { Annotation } from '@frank17008/image-annotation';
// CORRECT
import type { Annotation } from '@frank17008/image-annotation';
// WRONG - controlled without onChange (React warning)
<ImageAnnotation src="..." value={annotations} />
// CORRECT - controlled mode
<ImageAnnotation
src="..."
value={annotations}
onChange={setAnnotations}
/>
For images > 5MB:
The component redraws on each change. For performance: