@sudarsanank/react-pdf-annotations
v1.1.0
Published
A standalone React library for PDF annotations with real-time collaboration support
Maintainers
Readme
react-annotations
A powerful, standalone React library for PDF annotations with real-time collaboration support. Works with any PDF viewer library through an adapter pattern.
Features
- ✅ 4 Annotation Types: Highlights, Freehand Drawing, Shapes (Rectangle/Circle/Arrow), Notes
- ✅ Real-time Collaboration: Built-in Socket.IO support for multi-user editing
- ✅ Viewer Agnostic: Works with react-pdf, PDF.js, or any custom PDF viewer
- ✅ State Management: Redux Toolkit for robust state handling
- ✅ Storage Adapters: API backend, LocalStorage, or custom implementations
- ✅ TypeScript: Full type safety and IntelliSense support
- ✅ Tailwind CSS: Beautiful, customizable UI components
- ✅ React 19: Built for the latest React features
Installation
npm install @sudarsanank/react-pdf-annotations
# or
yarn add @sudarsanank/react-pdf-annotations
# or
pnpm add @sudarsanank/react-pdf-annotationsPeer Dependencies
npm install react react-dom @reduxjs/toolkit react-redux socket.io-client axiosQuick Start
Basic Usage with react-pdf
import { Document, Page } from 'react-pdf';
import {
AnnotationProvider,
AnnotationLayer,
AnnotationCanvas,
AnnotationToolbar,
NotesPanel,
createApiStorage,
createSocketIOCollaboration,
} from '@sudarsanank/react-pdf-annotations';
import '@sudarsanank/react-pdf-annotations/styles';
function PDFAnnotator() {
const storage = createApiStorage({
baseUrl: 'http://localhost:3001/api',
documentId: 'doc-123',
versionId: 'v-1',
});
const collaboration = createSocketIOCollaboration({
url: 'http://localhost:3001',
documentId: 'doc-123',
versionId: 'v-1',
userId: 'user-1',
});
return (
<AnnotationProvider
storage={storage}
collaboration={collaboration}
documentId="doc-123"
versionId="v-1"
userId="user-1"
config={{
tools: ['select', 'highlight', 'draw', 'rectangle', 'circle', 'arrow', 'note'],
defaultColor: '#FFEB3B',
colorPalette: ['#FFEB3B', '#FF5722', '#4CAF50', '#2196F3'],
}}
>
<div className="flex h-screen">
<div className="flex-1 flex flex-col">
<AnnotationToolbar />
<div className="flex-1 relative overflow-auto">
<Document file="/document.pdf">
<Page pageNumber={1} />
</Document>
{/* Annotation overlays */}
<AnnotationLayer />
<AnnotationCanvas />
</div>
</div>
<NotesPanel />
</div>
</AnnotationProvider>
);
}Offline Mode with LocalStorage
import {
AnnotationProvider,
createLocalStorage,
createNoOpCollaboration,
} from '@sudarsanank/react-pdf-annotations';
function OfflineAnnotator() {
const storage = createLocalStorage({
documentId: 'my-document',
versionId: 'v1',
});
return (
<AnnotationProvider
storage={storage}
collaboration={createNoOpCollaboration()}
documentId="my-document"
>
{/* Your UI components */}
</AnnotationProvider>
);
}Architecture
Adapter Pattern
The library uses adapters to decouple from specific implementations:
Storage Adapters
interface StorageAdapter {
fetchAnnotations(documentId: string, versionId?: string): Promise<Annotation[]>;
createAnnotation(annotation: CreateAnnotationDto): Promise<Annotation>;
updateAnnotation(annotationId: string, updates: Partial<Annotation>): Promise<Annotation>;
deleteAnnotation(annotationId: string): Promise<void>;
}Built-in Adapters:
ApiStorage- REST API backend (NestJS compatible)LocalStorage- Browser localStorage for offline use
Collaboration Adapters
interface CollaborationAdapter {
connect(): void;
disconnect(): void;
joinDocument(documentId: string, versionId?: string): void;
leaveDocument(documentId: string, versionId?: string): void;
emit(event: CollaborationEvent): void;
on(event: string, handler: (data: any) => void): void;
isConnected(): boolean;
}Built-in Adapters:
SocketIOAdapter- Real-time collaboration via Socket.IONoOpAdapter- Disables collaboration (single-user mode)
Renderer Adapters
interface RendererAdapter {
getPageDimensions(pageNumber: number): { width: number; height: number };
getCurrentPage(): number;
getTotalPages(): number;
getScale(): number;
getContainerElement(): HTMLElement | null;
}Built-in Adapters:
ReactPDFAdapter- Works with react-pdfGenericAdapter- Works with any PDF viewer
Components
AnnotationProvider
Root provider component that sets up the annotation environment.
<AnnotationProvider
storage={storageAdapter}
collaboration={collaborationAdapter}
renderer={rendererAdapter}
documentId="doc-123"
versionId="v-1"
userId="user-1"
config={{
tools: ['select', 'highlight', 'draw', 'note'],
defaultTool: 'select',
defaultColor: '#FFEB3B',
colorPalette: ['#FFEB3B', '#FF5722', '#4CAF50'],
ui: {
showToolbar: true,
showNotesPanel: true,
toolbarPosition: 'top',
notesPanelPosition: 'right',
},
permissions: {
canCreate: true,
canEdit: true,
canDelete: true,
canEditOthers: false,
},
callbacks: {
onAnnotationCreate: (annotation) => console.log('Created:', annotation),
onAnnotationUpdate: (annotation) => console.log('Updated:', annotation),
onAnnotationDelete: (id) => console.log('Deleted:', id),
onError: (error) => console.error('Error:', error),
},
}}
>
{children}
</AnnotationProvider>AnnotationLayer
Renders existing annotations on top of the PDF.
<AnnotationLayer />AnnotationCanvas
Captures user input for creating new annotations.
<AnnotationCanvas />AnnotationToolbar
Tool selection and color picker UI.
<AnnotationToolbar />NotesPanel
Sidebar panel for viewing and editing note annotations.
<NotesPanel />Hooks
useAnnotations
Main hook for annotation CRUD operations.
import { useAnnotations } from '@sudarsanank/react-pdf-annotations';
function MyComponent() {
const {
annotations,
loading,
error,
selected,
createAnnotation,
updateAnnotation,
deleteAnnotation,
selectAnnotation,
} = useAnnotations();
const handleCreate = async () => {
await createAnnotation({
documentVersionId: 'v-1',
userId: 'user-1',
annotationType: AnnotationType.HIGHLIGHT,
pageNumber: 1,
positionData: { x: 0.1, y: 0.1, width: 0.2, height: 0.05 },
color: '#FFEB3B',
});
};
return <div>{annotations.length} annotations</div>;
}useAnnotationDraw
Manages drawing state and active tool.
import { useAnnotationDraw } from '@sudarsanank/react-pdf-annotations';
function ToolSelector() {
const { currentTool, setTool, color, setColor } = useAnnotationDraw();
return (
<div>
<button onClick={() => setTool(AnnotationTool.HIGHLIGHT)}>
Highlight
</button>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
</div>
);
}useCollaboration
Manages real-time collaboration state.
import { useCollaboration } from '@sudarsanank/react-pdf-annotations';
function CollaborationStatus() {
const { isConnected } = useCollaboration();
return <div>{isConnected ? 'Connected' : 'Offline'}</div>;
}useViewport
Controls zoom and page navigation.
import { useViewport } from '@sudarsanank/react-pdf-annotations';
function Navigation() {
const {
currentPage,
totalPages,
scale,
nextPage,
previousPage,
zoomIn,
zoomOut,
} = useViewport();
return (
<div>
<button onClick={previousPage}>Previous</button>
<span>Page {currentPage} of {totalPages}</span>
<button onClick={nextPage}>Next</button>
<button onClick={zoomOut}>-</button>
<span>{Math.round(scale * 100)}%</span>
<button onClick={zoomIn}>+</button>
</div>
);
}Custom Adapters
Custom Storage Adapter
import { StorageAdapter, Annotation, CreateAnnotationDto } from '@sudarsanank/react-pdf-annotations';
class MyCustomStorage implements StorageAdapter {
async fetchAnnotations(documentId: string): Promise<Annotation[]> {
// Your implementation
return await myApi.getAnnotations(documentId);
}
async createAnnotation(dto: CreateAnnotationDto): Promise<Annotation> {
return await myApi.createAnnotation(dto);
}
async updateAnnotation(id: string, updates: Partial<Annotation>): Promise<Annotation> {
return await myApi.updateAnnotation(id, updates);
}
async deleteAnnotation(id: string): Promise<void> {
await myApi.deleteAnnotation(id);
}
}
// Use it
const storage = new MyCustomStorage();Custom Collaboration Adapter
import { CollaborationAdapter, CollaborationEvent } from '@sudarsanank/react-pdf-annotations';
class MyWebSocketAdapter implements CollaborationAdapter {
private ws: WebSocket | null = null;
connect(): void {
this.ws = new WebSocket('ws://localhost:8080');
}
disconnect(): void {
this.ws?.close();
}
emit(event: CollaborationEvent): void {
this.ws?.send(JSON.stringify(event));
}
// ... implement other methods
}Configuration Options
interface AnnotationConfig {
tools?: AnnotationTool[];
defaultTool?: AnnotationTool;
defaultColor?: string;
colorPalette?: string[];
ui?: {
showToolbar?: boolean;
showNotesPanel?: boolean;
toolbarPosition?: 'top' | 'bottom' | 'left' | 'right';
notesPanelPosition?: 'left' | 'right';
theme?: 'light' | 'dark' | 'auto';
};
behavior?: {
autoSave?: boolean;
autoSaveDelay?: number;
enableUndo?: boolean;
maxUndoSteps?: number;
enableKeyboardShortcuts?: boolean;
selectAfterCreate?: boolean;
};
permissions?: {
canCreate?: boolean;
canEdit?: boolean;
canDelete?: boolean;
canEditOthers?: boolean;
};
callbacks?: {
onAnnotationCreate?: (annotation: Annotation) => void;
onAnnotationUpdate?: (annotation: Annotation) => void;
onAnnotationDelete?: (annotationId: string) => void;
onToolChange?: (tool: AnnotationTool) => void;
onError?: (error: Error) => void;
};
}TypeScript Support
The library is fully typed. Import types as needed:
import type {
Annotation,
AnnotationType,
AnnotationTool,
CreateAnnotationDto,
UpdateAnnotationDto,
StorageAdapter,
CollaborationAdapter,
RendererAdapter,
AnnotationConfig,
} from '@sudarsanank/react-pdf-annotations';Styling
The library uses Tailwind CSS. Import the styles:
import '@sudarsanank/react-pdf-annotations/styles';For custom styling, override the Tailwind classes or use your own CSS:
.annotation-layer {
/* Custom styles */
}
.annotation-toolbar {
/* Custom toolbar styles */
}Backend Integration
NestJS Example
The library works seamlessly with a NestJS backend:
// NestJS Controller
@Controller('documents/:documentId/versions/:versionId/annotations')
export class AnnotationsController {
@Get()
async list(@Param('documentId') documentId: string, @Param('versionId') versionId: string) {
return await this.annotationsService.findAll(documentId, versionId);
}
@Post()
async create(@Body() dto: CreateAnnotationDto) {
return await this.annotationsService.create(dto);
}
@Put(':id')
async update(@Param('id') id: string, @Body() updates: UpdateAnnotationDto) {
return await this.annotationsService.update(id, updates);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return await this.annotationsService.delete(id);
}
}WebSocket Gateway
@WebSocketGateway()
export class AnnotationsGateway {
@SubscribeMessage('joinDocument')
handleJoinDocument(@MessageBody() data: { documentId: string; versionId: string }) {
// Handle room joining
}
@SubscribeMessage('createAnnotation')
async handleCreateAnnotation(@MessageBody() data: any) {
const annotation = await this.annotationsService.create(data.annotation);
this.server.to(`${data.documentId}-${data.versionId}`).emit('annotationCreated', annotation);
}
}API Documentation
Comprehensive API documentation is available and can be generated locally:
# Generate API documentation
npm run docs
# Serve documentation locally at http://localhost:3002
npm run docs:serveThe documentation includes:
- Complete API reference for all exported components, hooks, and types
- TypeScript type definitions
- Usage examples and descriptions
- Source code links
Contributing
Contributions are welcome! Please read our contributing guidelines.
License
MIT
Support
For issues and questions, please visit our GitHub repository.
