This document presents a detailed low-level design for a whiteboard application that supports real-time collaborative drawing, multi-user interactions, and scalable frontend architecture with efficient rendering and state management.
1. Problem Statement & Requirements
Clarifying questions
- What's the target browser support matrix? (e.g., modern browsers only, IE11 support needed?)
- Do we need real-time collaborative editing capabilities or is it primarily single-user?
- Should the whiteboard support offline mode with sync-on-reconnect?
- What level of accessibility is required? (e.g., WCAG 2.1 AA compliance, screen reader support)
- Do we need internationalization support for multiple languages?
- What's the expected maximum number of concurrent users and whiteboard size/complexity?
- Are there specific design system or component library constraints we should follow?
Functional requirements (user-facing)
- Core drawing tools: Pen, line, rectangle, circle, text input, eraser
- Canvas manipulation: Zoom in/out, panning, canvas reset
- Shape management: Select, move, resize, delete, duplicate shapes
- Layer management: Create, organize, hide/show layers
- Export functionality: Save as PNG, SVG, or PDF
- Undo/redo: Multi-step undo/redo functionality with history tracking
- Color palette: Custom color selection and preset colors
- Brush size control: Adjustable stroke width for drawing tools
Non-functional requirements
- Performance targets:
- LCP < 2.5s, INP < 200ms, CLS < 0.1 for interactive elements
- Canvas should support 1000+ shapes without significant lag
- Accessibility: WCAG 2.1 AA compliance with keyboard navigation and screen reader support
- Scalability: Support for whiteboards up to 10k+ elements with efficient rendering
- Responsiveness: Mobile and desktop support with touch/pen input handling
Constraints and assumptions
- Assumption: Canvas size is bounded (e.g., max 2000x2000 pixels)
- Constraint: No external drawing libraries (must implement core rendering logic)
- Assumption: User has basic computer literacy and can use mouse/touch input
- Constraint: All whiteboard data is stored in client-side state (no server persistence for this design)
- Assumption: Canvas canvas is rendered using HTML5 Canvas API
Out of scope
- Multi-user real-time collaboration features (WebSockets, CRDTs)
- Advanced shape manipulation (rotation, skewing, complex transforms)
- Cloud storage integration or file upload/download to external services
- Advanced animation or transition effects
- Print functionality beyond export to image formats
- Third-party plugin system or extensibility framework
2. Component Architecture
Component Tree Diagram
graph TD
A[WhiteboardApp] --> B[Toolbar]
A --> C[Canvas]
A --> D[StatusBar]
A --> E[ModalManager]
B --> B1[ToolSelector]
B --> B2[ColorPicker]
B --> B3[BrushSizeSlider]
B --> B4[UndoRedoButtons]
C --> C1[DrawingSurface]
C --> C2[ShapeRenderer]
C --> C3[TextEditor]
C --> C4[SelectionManager]
E --> E1[Modal]
E --> E2[ConfirmationDialog]
E --> E3[ExportModal]
Component Breakdown
Smart Components (Container Components):
WhiteboardApp- Main application container that orchestrates the entire whiteboard experienceToolbar- Manages toolbar state and tool selectionCanvas- Core drawing surface with event handling and rendering logicModalManager- Centralized modal state management
Dumb Components (Presentational Components):
ToolSelector- Renders available drawing toolsColorPicker- Color selection UI elementBrushSizeSlider- Brush size adjustment componentUndoRedoButtons- History navigation controlsDrawingSurface- Raw canvas rendering layerShapeRenderer- Shape drawing and rendering logicTextEditor- Text input and display componentSelectionManager- Selection and manipulation controls
TypeScript Props Contracts
// WhiteboardApp.tsx
interface WhiteboardAppProps {
initialContent?: WhiteboardContent;
userId?: string;
roomId?: string;
isRealtime?: boolean;
onContentChange?: (content: WhiteboardContent) => void;
}
// Toolbar.tsx
interface ToolbarProps {
activeTool: ToolType;
selectedColor: string;
brushSize: number;
canUndo: boolean;
canRedo: boolean;
onToolChange: (tool: ToolType) => void;
onColorChange: (color: string) => void;
onBrushSizeChange: (size: number) => void;
onUndo: () => void;
onRedo: () => void;
}
// Canvas.tsx
interface CanvasProps {
content: WhiteboardContent;
activeTool: ToolType;
selectedColor: string;
brushSize: number;
isDrawing: boolean;
onContentUpdate: (content: WhiteboardContent) => void;
onDrawingStart: () => void;
onDrawingEnd: () => void;
}
// ModalManager.tsx
interface ModalManagerProps {
modals: ModalState[];
onCloseModal: (id: string) => void;
onConfirm: (id: string, data?: any) => void;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Drawing: StartDrawing
Drawing --> Idle: StopDrawing
Drawing --> Erasing: ChangeTool(Eraser)
Erasing --> Drawing: ChangeTool(Pencil)
Erasing --> Idle: StopDrawing
Idle --> Selecting: StartSelecting
Selecting --> Idle: StopSelecting
Selecting --> Moving: MoveSelection
Moving --> Idle: StopMoving
state Drawing {
[*] --> DrawingStart
DrawingStart --> DrawingInProgress: MouseMove
DrawingInProgress --> DrawingEnd: MouseUp
DrawingEnd --> [*]
}
state Selecting {
[*] --> SelectionStart
SelectionStart --> SelectionEnd: MouseUp
SelectionEnd --> [*]
}
Component Responsibilities and Props Contract
WhiteboardApp:
- Responsible for application-wide state management
- Handles real-time collaboration if enabled
- Manages content persistence and synchronization
- Props:
initialContent,userId,roomId,isRealtime,onContentChange
Toolbar:
- Manages toolbar state and user interactions
- Communicates tool changes to Canvas
- Props:
activeTool,selectedColor,brushSize,canUndo,canRedo, and callback handlers
Canvas:
- Core drawing surface with event handling
- Renders all whiteboard elements
- Manages drawing state and user interactions
- Props:
content,activeTool,selectedColor,brushSize,isDrawing, and update handlers
Reusability Decisions
- Highly reusable:
ColorPicker,BrushSizeSlider,UndoRedoButtons - Feature-specific:
DrawingSurface,ShapeRenderer(tied to whiteboard functionality) - Context-aware:
ToolbarandStatusBarare tightly coupled to whiteboard context
Component Lifecycle Considerations
- Mounting: Initialize content from props or defaults, set up event listeners
- Updating: Handle content changes, tool/brush updates, and real-time sync
- Unmounting: Clean up event listeners, cancel ongoing operations, save state if needed
- Performance: Implement proper memoization for expensive rendering operations
The component architecture prioritizes separation of concerns while maintaining tight coupling between related components like Canvas and Toolbar, which need to coordinate closely for a seamless whiteboard experience.
3. State Management Design
Local state vs. global state decisions
For a whiteboard application, we need to balance between local component state and global state management. Local state is appropriate for:
- Canvas element's internal drawing state (current tool, stroke points)
- UI component states (active tool, modal visibility, toolbar open/closed)
- Temporary UI interactions (hover states, drag previews)
Global state is required for:
- Whiteboard document content and metadata
- Multi-user collaboration state
- User preferences and settings
- Cross-component coordination (e.g., active tool affecting multiple UI elements)
State management approach
Zustand is selected as the primary state management solution for this whiteboard application.
Justification:
- Simplicity: Zustand provides a clean, minimal API that's easy to understand and maintain
- TypeScript native: Excellent type inference and support for complex nested state structures
- No boilerplate: Unlike Redux, it doesn't require action creators or reducers
- Middleware support: Can easily integrate with devtools and logging middleware
- Performance: Optimized for React's rendering model with automatic re-renders
- Scalability: Can be easily extended to support middleware like persistence or undo/redo
State shape and normalization strategy
// Core whiteboard state structure
interface WhiteboardState {
document: {
id: string;
title: string;
content: WhiteboardContent;
metadata: DocumentMetadata;
};
user: {
id: string;
name: string;
color: string;
};
collaboration: {
users: Collaborator[];
activeUsers: ActiveUser[];
cursorPositions: Record<string, CursorPosition>;
};
ui: {
activeTool: ToolType;
isDrawing: boolean;
zoomLevel: number;
panOffset: { x: number; y: number };
selectedElements: string[];
isFullscreen: boolean;
};
}
interface WhiteboardContent {
elements: WhiteboardElement[];
history: HistoryState;
}
interface DocumentMetadata {
createdAt: Date;
updatedAt: Date;
version: number;
collaborators: string[];
}
Server state vs. client state separation
Client State (local to user):
- UI state (active tool, fullscreen mode)
- Canvas pan/zoom state
- Temporary drawing states (stroke preview)
- Local UI preferences
Server State (shared across users):
- Document content and elements
- Collaboration metadata (active users, cursors)
- Version history and sync state
Cache invalidation strategy
- Document-level invalidation: When document content changes, invalidate all cached elements
- Element-specific invalidation: When individual elements change, only update that element's cache
- Time-based invalidation: For collaborative content, implement TTL for cursor positions and active user states
- Manual invalidation: When user explicitly triggers refresh or undo/redo operations
Optimistic updates design
For collaborative whiteboard features, optimistic updates are crucial for responsive UI:
- Local operation: User draws a line → immediately render to canvas
- Server sync: Send to backend with timestamp and operation ID
- Conflict resolution: If server rejects operation, rollback local changes
- Real-time updates: When other users draw, apply to local state with appropriate delay handling
Critical-path hook implementation
// WhiteboardStore.ts - Core state management hook
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface WhiteboardElement {
id: string;
type: 'rectangle' | 'circle' | 'line' | 'text';
x: number;
y: number;
width?: number;
height?: number;
strokeColor: string;
strokeWidth: number;
text?: string;
points?: { x: number; y: number }[];
}
interface HistoryState {
past: WhiteboardElement[][];
future: WhiteboardElement[][];
currentIndex: number;
}
interface Collaborator {
id: string;
name: string;
color: string;
lastActive: Date;
}
interface CursorPosition {
x: number;
y: number;
userId: string;
}
interface WhiteboardStore {
// Document state
documentId: string | null;
title: string;
elements: WhiteboardElement[];
history: HistoryState;
// User state
currentUser: {
id: string;
name: string;
color: string;
} | null;
// Collaboration state
collaborators: Collaborator[];
cursorPositions: Record<string, CursorPosition>;
// UI state
activeTool: 'select' | 'rectangle' | 'circle' | 'line' | 'text';
isDrawing: boolean;
zoomLevel: number;
panOffset: { x: number; y: number };
selectedElements: string[];
// Actions
setDocumentId: (id: string) => void;
setTitle: (title: string) => void;
addElement: (element: WhiteboardElement) => void;
updateElement: (id: string, updates: Partial<WhiteboardElement>) => void;
deleteElement: (id: string) => void;
undo: () => void;
redo: () => void;
setActiveTool: (tool: WhiteboardStore['activeTool']) => void;
setIsDrawing: (isDrawing: boolean) => void;
setZoomLevel: (zoom: number) => void;
setPanOffset: (offset: { x: number; y: number }) => void;
selectElement: (id: string) => void;
deselectElement: (id: string) => void;
clearSelection: () => void;
// Collaboration actions
addCollaborator: (collaborator: Collaborator) => void;
removeCollaborator: (userId: string) => void;
updateCursorPosition: (userId: string, position: CursorPosition) => void;
// Sync operations
syncDocument: (remoteElements: WhiteboardElement[]) => void;
}
const useWhiteboardStore = create<WhiteboardStore>()(
devtools(
(set, get) => ({
// Initial state
documentId: null,
title: 'Untitled Whiteboard',
elements: [],
history: {
past: [],
future: [],
currentIndex: -1,
},
currentUser: null,
collaborators: [],
cursorPositions: {},
activeTool: 'select',
isDrawing: false,
zoomLevel: 1,
panOffset: { x: 0, y: 0 },
selectedElements: [],
// Document actions
setDocumentId: (id) => set({ documentId: id }),
setTitle: (title) => set({ title }),
addElement: (element) => {
const currentElements = get().elements;
set({
elements: [...currentElements, element],
});
},
updateElement: (id, updates) => {
set((state) => ({
elements: state.elements.map((el) =>
el.id === id ? { ...el, ...updates } : el
),
}));
},
deleteElement: (id) => {
set((state) => ({
elements: state.elements.filter((el) => el.id !== id),
}));
},
// History actions
undo: () => {
set((state) => {
if (state.history.currentIndex < 0) return state;
const newPast = [...state.history.past];
const newFuture = [...state.history.future];
const currentElements = newPast.pop() || [];
return {
elements: currentElements,
history: {
past: newPast,
future: [...newFuture, state.elements],
currentIndex: state.history.currentIndex - 1,
},
};
});
},
redo: () => {
set((state) => {
if (state.history.future.length === 0) return state;
const newFuture = [...state.history.future];
const currentElements = newFuture.pop() || [];
return {
elements: currentElements,
history: {
past: [...state.history.past, state.elements],
future: newFuture,
currentIndex: state.history.currentIndex + 1,
},
};
});
},
// UI actions
setActiveTool: (tool) => set({ activeTool: tool }),
setIsDrawing: (isDrawing) => set({ isDrawing }),
setZoomLevel: (zoom) => set({ zoomLevel: zoom }),
setPanOffset: (offset) => set({ panOffset: offset }),
selectElement: (id) => {
set((state) => ({
selectedElements: [...state.selectedElements, id],
}));
},
deselectElement: (id) => {
set((state) => ({
selectedElements: state.selectedElements.filter(
(selectedId) => selectedId !== id
),
}));
},
clearSelection: () => set({ selectedElements: [] }),
// Collaboration actions
addCollaborator: (collaborator) =>
set((state) => ({
collaborators: [...state.collaborators, collaborator],
})),
removeCollaborator: (userId) =>
set((state) => ({
collaborators: state.collaborators.filter(
(collab) => collab.id !== userId
),
})),
updateCursorPosition: (userId, position) =>
set((state) => ({
cursorPositions: {
...state.cursorPositions,
[userId]: position,
},
})),
// Sync operations
syncDocument: (remoteElements) => {
set({
elements: remoteElements,
});
},
}),
{ name: 'whiteboard-store' }
)
);
export default useWhiteboardStore;
// Custom hook for whiteboard operations with side effects
export const useWhiteboardOperations = () => {
const store = useWhiteboardStore();
// Handle element creation with optimistic update and sync
const createElement = (element: WhiteboardElement) => {
// Optimistic update - immediately show to user
store.addElement(element);
// Async sync with backend (with retry logic)
const syncWithBackend = async () => {
try {
await fetch('/api/whiteboard/elements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentId: store.documentId,
element,
}),
});
} catch (error) {
console.error('Failed to sync element:', error);
// Rollback local state if sync fails
store.deleteElement(element.id);
}
};
// Schedule sync (could use debounce for performance)
setTimeout(syncWithBackend, 100);
};
return {
...store,
createElement,
};
};
// Example usage in component
const WhiteboardCanvas = () => {
const { elements, activeTool, isDrawing, setZoomLevel } = useWhiteboardOperations();
const handleZoom = (delta: number) => {
setZoomLevel(prev => Math.max(0.1, Math.min(3, prev + delta)));
};
return (
<div className="whiteboard-canvas">
{elements.map(element => (
<WhiteboardElement key={element.id} element={element} />
))}
</div>
);
};
4. Routing & Navigation
Route Structure and Hierarchy
The whiteboard application will follow a clean route structure with clear separation between public and protected routes:
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> D[/login]
B --> E[/signup]
B --> F[/invite/:id]
C --> G[/boards]
C --> H[/boards/:id]
C --> I[/boards/:id/collaborators]
Protected routes (/boards, /boards/:id) will require authentication, while public routes handle authentication flows and invitations. The board detail route (/boards/:id) will support deep linking for sharing boards.
Code Splitting Strategy
Route-based code splitting with dynamic imports:
-
Public Routes (
/login,/signup,/invite)- Split into separate chunks to reduce initial bundle size
- These routes are not frequently accessed and can be lazy-loaded
-
Board Routes (
/boards,/boards/:id)- Split into separate chunks using React.lazy and Suspense
- Primary board view will be loaded on demand when user navigates to
/boards/:id - Whiteboard component will use dynamic imports for heavy libraries (e.g., Fabric.js)
-
Collaboration Features (
/boards/:id/collaborators)- Split into separate chunk as these are feature-specific and not always needed
// Example route configuration with lazy loading
const routes = [
{
path: '/login',
element: React.lazy(() => import('pages/LoginPage')),
},
{
path: '/boards/:id',
element: React.lazy(() => import('pages/WhiteboardPage')),
children: [
{
path: 'collaborators',
element: React.lazy(() => import('pages/CollaboratorsPage')),
}
]
}
];
Navigation Guards and Authentication Flows
Authentication guards will be implemented using React Router's useNavigate hook and context:
// ProtectedRoute.tsx
const ProtectedRoute: React.FC = () => {
const { isAuthenticated, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && !isAuthenticated) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, loading, navigate]);
if (loading) return <LoadingSpinner />;
return isAuthenticated ? <Outlet /> : null;
};
Deep Linking Support and URL Parameter Design
URL structure will support deep linking with clear parameter patterns:
/boards/:boardId?tab=collaborators&zoom=100&pan=x,y
Key URL parameters:
boardId: Unique identifier for the whiteboardtab: Active tab (collaborators, settings)zoom: Current zoom level (0.5-2.0)pan: Pan position as x,y coordinates
Browser History Management and Scroll Restoration
// App.tsx - scroll restoration setup
import { useScrollRestoration } from 'react-router-dom';
function App() {
useScrollRestoration();
return (
<BrowserRouter>
<Routes>
{/* ... routes */}
</Routes>
</BrowserRouter>
);
}
Scroll restoration will be handled automatically by React Router v6, but custom scroll behavior may be implemented for whiteboard-specific needs:
// Custom scroll restoration for whiteboard view
const WhiteboardPage: React.FC = () => {
const location = useLocation();
useEffect(() => {
// Restore scroll position when navigating back to whiteboard
if (location.state?.scrollPosition) {
window.scrollTo(
location.state.scrollPosition.x,
location.state.scrollPosition.y
);
}
}, [location]);
return (
<div className="whiteboard-container">
{/* Whiteboard content */}
</div>
);
};
5. API Integration Layer
API Client Design Pattern
We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent request handling and error management across the whiteboard application.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api';
const createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding auth token
client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for handling errors
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return client;
};
export const apiClient = createApiClient();
Request/Response Interceptors
The interceptors handle authentication token injection, response normalization, and error transformation to provide a consistent API experience.
Error Handling and Retry Strategy
We implement exponential backoff retry logic for transient network errors:
// api/retry.ts
export const retryWithBackoff = async <T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> => {
let lastError: Error;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i === maxRetries || !isRetryableError(error)) {
throw lastError;
}
const delay = baseDelay * Math.pow(2, i);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
const isRetryableError = (error: any): boolean => {
return (
error?.response?.status === 408 || // Request timeout
error?.response?.status === 429 || // Rate limit
error?.response?.status === 500 || // Server error
error?.response?.status === 502 || // Bad gateway
error?.response?.status === 503 || // Service unavailable
error?.code === 'ECONNABORTED' || // Timeout
error?.code === 'ENOTFOUND' || // Network error
error?.code === 'ECONNREFUSED' // Connection refused
);
};
Loading, Error, and Empty States Design
We implement a standardized loading state pattern using React Query's built-in loading states:
// components/WhiteboardList.tsx
import { useQuery } from '@tanstack/react-query';
import { fetchWhiteboards } from '@/api/whiteboard';
const WhiteboardList = () => {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['whiteboards'],
queryFn: () => fetchWhiteboards(),
});
if (isLoading) return <div className="loading">Loading whiteboards...</div>;
if (isError) {
return (
<div className="error">
<p>Failed to load whiteboards</p>
<button onClick={() => refetch()}>Retry</button>
</div>
);
}
if (!data || data.length === 0) {
return <div className="empty">No whiteboards found</div>;
}
return (
<ul>
{data.map(whiteboard => (
<li key={whiteboard.id}>{whiteboard.name}</li>
))}
</ul>
);
};
Data Fetching Strategy
For a whiteboard application, we use a hybrid approach:
- CSR (Client-Side Rendering) for the main whiteboard editor and user dashboard
- SSR for public whiteboard views to improve SEO and initial load performance
- Server-Side Rendering with ISR for collaborative whiteboard views that need to be updated in real-time
// pages/whiteboard/[id].tsx (SSR example)
import { GetServerSideProps } from 'next';
import { getWhiteboardById } from '@/api/whiteboard';
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
try {
const whiteboard = await getWhiteboardById(params.id as string);
// For collaborative whiteboards, we might want to fetch initial state
const initialData = await getWhiteboardState(params.id as string);
return {
props: {
whiteboard,
initialData,
},
};
} catch (error) {
return {
notFound: true,
};
}
};
API Response Transformation Layer
We implement a DTO-to-domain model mapping layer to normalize data:
// api/transformers.ts
import { Whiteboard, WhiteboardState } from '@/types/whiteboard';
export const transformWhiteboardResponse = (dto: any): Whiteboard => ({
id: dto.id,
name: dto.name,
createdAt: new Date(dto.createdAt),
updatedAt: new Date(dto.updatedAt),
ownerId: dto.ownerId,
collaborators: dto.collaborators?.map((c: any) => ({
id: c.id,
name: c.name,
avatarUrl: c.avatarUrl,
})) || [],
});
export const transformWhiteboardStateResponse = (dto: any): WhiteboardState => ({
id: dto.id,
elements: dto.elements.map((el: any) => ({
id: el.id,
type: el.type,
content: el.content,
position: {
x: el.position.x,
y: el.position.y,
},
size: {
width: el.size.width,
height: el.size.height,
},
})),
version: dto.version,
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant U as User
participant C as Client (React)
participant A as API Server
participant D as Database
U->>C: Create new whiteboard
C->>A: POST /whiteboards
A->>D: Insert new whiteboard record
D-->>A: Return created whiteboard
A-->>C: Response with whiteboard data
C-->>U: Display new whiteboard
U->>C: Save whiteboard state
C->>A: PUT /whiteboards/{id}/state
A->>D: Update whiteboard state
D-->>A: Confirm update
A-->>C: Success response
C->>U: Show success feedback
U->>C: Load whiteboard
C->>A: GET /whiteboards/{id}
A->>D: Fetch whiteboard data
D-->>A: Return whiteboard details
A-->>C: Response with whiteboard data
C-->>U: Render whiteboard UI
U->>C: Collaborator join
C->>A: POST /whiteboards/{id}/collaborators
A->>D: Add collaborator to whiteboard
D-->>A: Confirm addition
A-->>C: Notify all collaborators of new member
This API integration layer provides a robust foundation for the whiteboard application with proper error handling, retry logic, and data normalization that supports both real-time collaboration and offline-first experiences. The design allows for easy scaling to support multiple whiteboard types, versioning, and complex collaborative features.
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for whiteboard content loading
- Interaction to Next Paint (INP): <200ms for user interactions (drawing, panning, zooming)
- Cumulative Layout Shift (CLS): <0.1 for all interactive elements and content
Bundle Size Budget per Route
- Whiteboard Editor Route: <500KB (including all dependencies and assets)
- Toolbar & UI Components: <150KB total
- Drawing Tools & Canvas Libraries: <200KB (including canvas libraries and SVG handling)
- File Management & Export: <100KB
Critical Rendering Path Optimization
- Critical CSS: Inline essential styles for toolbar, canvas container, and core UI elements
- Lazy Loading: Non-critical components (color picker, font selector) loaded on demand
- Resource Hints: Preload canvas libraries and core assets for faster rendering
- Code Splitting: Route-based splitting with dynamic imports for heavy modules
Image Optimization Strategy
- Canvas Export Format: WebP for high-quality exports, JPEG for low-memory scenarios
- Lazy Loading: Canvas elements rendered only when visible in viewport
- Responsive Images: Canvas content scaled based on device pixel ratio for crisp rendering
- Compression: Lossless compression for vector-based whiteboard elements, lossy for rasterized exports
Font Loading Strategy
- Font Display:
swapfor all UI fonts to prevent FOIT (Flash of Invisible Text) - Preload: Critical font subsets for toolbar and UI elements
- Fallback Fonts: System fonts as fallback to ensure content visibility during font loading
Memoization Strategy
- React.memo: Wrap all UI component containers with
React.memowhere props change infrequently - useCallback: Memoize event handlers for drawing operations and UI interactions
- useMemo: Cache computed canvas states, element transformations, and layout calculations
- Avoid Over-Memoization: Don't memoize frequently changing state like mouse coordinates or real-time drawing
Virtualization for Large Lists
- Element Lists: Use
react-windowfor large lists of whiteboard elements (e.g., when displaying hundreds of shapes) - Canvas Rendering: Implement virtualized canvas rendering for large whiteboards using viewports and element culling
- Scrolling Performance: Use
requestAnimationFramefor smooth panning and zooming with debounced rendering
Prefetching and Preloading Strategy
- Preload Canvas Libraries: Load
fabric.jsorkonva.json app initialization - Preload Recent Documents: Fetch recent whiteboard metadata when user lands on dashboard
- Prefetch UI Components: Load color palette and tool options when whiteboard route is accessed
- Service Worker Caching: Cache frequently used assets (icons, default templates) for offline access
// Performance optimization example for whiteboard canvas rendering
const WhiteboardCanvas = React.memo(() => {
const [canvas, setCanvas] = useState<CanvasType | null>(null);
const [renderedElements, setRenderedElements] = useState<Element[]>([]);
// Memoize canvas size calculations
const canvasSize = useMemo(() => {
return { width: window.innerWidth, height: window.innerHeight - 100 };
}, []);
// Memoize element transformations to prevent unnecessary re-renders
const transformedElements = useMemo(() => {
return renderedElements.map(el => ({
...el,
transform: calculateTransform(el.position, canvas.zoom, canvas.pan)
}));
}, [renderedElements, canvas]);
// Debounced rendering to prevent excessive re-renders during panning
const debouncedRender = useCallback(
debounce(() => {
canvas?.renderAll();
}, 16), // ~60fps
[canvas]
);
useEffect(() => {
const canvasInstance = new fabric.Canvas('canvas', {
width: canvasSize.width,
height: canvasSize.height,
selection: false
});
setCanvas(canvasInstance);
}, [canvasSize]);
return (
<div className="whiteboard-container">
<canvas
id="canvas"
width={canvasSize.width}
height={canvasSize.height}
className="whiteboard-canvas"
/>
</div>
);
});
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while providing meaningful feedback to users. The component tree for error boundaries:
graph TD
A[App] --> B[WhiteboardContainer]
B --> C[Toolbar]
B --> D[Canvas]
B --> E[PropertiesPanel]
D --> F[DrawingElement]
D --> G[SelectionManager]
A --> H[ErrorBoundary]
B --> H
D --> H
E --> H
Key placement decisions:
- Top-level error boundary for the entire whiteboard application to catch unhandled errors
- Canvas-specific error boundary to handle drawing/rendering issues without crashing the entire app
- Component-level boundaries for critical sub-components like selection manager and toolbar
Global Error Handler Design
// src/core/errorHandler.ts
import { captureException } from '@sentry/browser';
interface GlobalErrorHandler {
handleError: (error: Error, errorInfo?: React.ErrorInfo) => void;
handleNetworkError: (error: Error, retryCallback?: () => void) => void;
}
class ErrorHandler implements GlobalErrorHandler {
handleError(error: Error, errorInfo?: React.ErrorInfo): void {
console.error('Unhandled error:', error, errorInfo);
// Report to monitoring service
captureException(error, {
contexts: {
react: errorInfo?.componentStack,
},
});
// Show user-friendly error message
this.showUserError(error.message);
}
handleNetworkError(error: Error, retryCallback?: () => void): void {
const networkError = new Error('Network error occurred. Please check your connection.');
// Show retryable toast
this.showNetworkErrorToast(networkError.message, retryCallback);
}
private showUserError(message: string): void {
// Implementation for showing user-friendly error UI
}
private showNetworkErrorToast(message: string, retryCallback?: () => void): void {
// Implementation for network error toast with retry
}
}
export const globalErrorHandler = new ErrorHandler();
Offline / Network Failure Handling
// src/core/offlineManager.ts
import { useEffect, useState } from 'react';
interface OfflineState {
isOnline: boolean;
lastSynced: Date | null;
pendingChanges: number;
}
export const useOfflineManager = (): OfflineState => {
const [offlineState, setOfflineState] = useState<OfflineState>({
isOnline: navigator.onLine,
lastSynced: null,
pendingChanges: 0,
});
useEffect(() => {
const handleOnline = () => {
setOfflineState(prev => ({ ...prev, isOnline: true }));
// Attempt to sync pending changes
attemptSync();
};
const handleOffline = () => {
setOfflineState(prev => ({ ...prev, isOnline: false }));
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const attemptSync = async () => {
// Sync pending changes with backend
};
return offlineState;
};
Graceful Degradation Patterns
- Drawing Mode Fallback: If WebGL fails, fall back to Canvas2D rendering
- Feature Degradation: Disable real-time collaboration if WebSocket connection fails
- Performance Degradation: Reduce drawing quality or simplify rendering when CPU/memory is constrained
// src/core/renderingFallback.ts
class RenderingFallback {
private fallbackRenderer: CanvasRenderingContext2D | null = null;
private originalRenderer: WebGLRenderingContext | null = null;
attemptRendering(canvas: HTMLCanvasElement): boolean {
try {
// Try WebGL first
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
this.originalRenderer = gl;
return true;
}
// Fallback to Canvas2D
const ctx = canvas.getContext('2d');
if (ctx) {
this.fallbackRenderer = ctx;
return true;
}
return false;
} catch (error) {
console.warn('Rendering fallback failed:', error);
return false;
}
}
getRenderer(): CanvasRenderingContext2D | WebGLRenderingContext | null {
return this.originalRenderer || this.fallbackRenderer;
}
}
User Feedback on Errors
// src/components/ToastManager.tsx
import React, { useState, useEffect } from 'react';
interface Toast {
id: string;
message: string;
type: 'error' | 'warning' | 'success';
duration?: number;
actions?: Array<{ label: string; onClick: () => void }>;
}
const ToastManager = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (toast: Omit<Toast, 'id'>) => {
const newToast = {
...toast,
id: Date.now().toString(),
};
setToasts(prev => [...prev, newToast]);
if (toast.duration) {
setTimeout(() => {
removeToast(newToast.id);
}, toast.duration);
}
};
const removeToast = (id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return (
<div className="toast-container">
{toasts.map(toast => (
<div
key={toast.id}
className={`toast toast--${toast.type}`}
>
<span>{toast.message}</span>
{toast.actions?.map((action, index) => (
<button
key={index}
onClick={() => {
action.onClick();
removeToast(toast.id);
}}
>
{action.label}
</button>
))}
</div>
))}
</div>
);
};
export default ToastManager;
Error Boundary Component
// src/components/ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: React.ReactNode; fallback?: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry, but there was an error in the whiteboard application.</p>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Retry Strategy Implementation
// src/core/retryHandler.ts
type RetryConfig = {
maxRetries?: number;
delay?: number;
backoffMultiplier?: number;
};
const retryWithBackoff = async <T>(
operation: () => Promise<T>,
config: RetryConfig = {}
): Promise<T> => {
const { maxRetries = 3, delay = 1000, backoffMultiplier = 2 } = config;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff
const waitTime = delay * Math.pow(backoffMultiplier, attempt);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
throw lastError;
};
export { retryWithBackoff };
Error Recovery Patterns
- State Recovery: Save whiteboard state to localStorage and restore on app restart
- Undo Stack Recovery: Maintain undo history in local storage for recovery after crashes
- Document State Recovery: Track document version and handle conflict resolution on reconnect
// src/core/stateRecovery.ts
class WhiteboardStateRecovery {
private localStorageKey = 'whiteboard-state';
saveState(state: any): void {
try {
localStorage.setItem(this.localStorageKey, JSON.stringify(state));
} catch (error) {
console.warn('Failed to save whiteboard state:', error);
}
}
restoreState(): any {
try {
const saved = localStorage.getItem(this.localStorageKey);
return saved ? JSON.parse(saved) : null;
} catch (error) {
console.warn('Failed to restore whiteboard state:', error);
return null;
}
}
clearState(): void {
localStorage.removeItem(this.localStorageKey);
}
}
8. Testing Strategy
Unit Testing
Component Tests (80-90% coverage):
- Core drawing components (pen, shape tools, text)
- Toolbar and tool selection logic
- Canvas interaction handlers (mousedown, mousemove, mouseup)
- State management hooks (useWhiteboardState, useToolSelection)
- Utility functions for coordinate transformations and shape calculations
Utility Tests (90%+ coverage):
- Coordinate math utilities (distance, midpoint, bounding box calculations)
- Shape serialization/deserialization logic
- Event handler normalization (mouse/touch event handling)
- Canvas rendering helpers
Target Coverage: 85% component test coverage, 95% utility test coverage
Integration Testing Strategy
Feature Integration Tests (60-70% coverage):
- Tool switching and state persistence across tools
- Multi-user collaboration flow (simulated)
- Canvas undo/redo functionality with multiple operations
- File save/load operations with various formats (PNG, SVG, JSON)
- Canvas zoom and pan interactions
Cross-component Tests:
- Toolbar ↔ Canvas communication
- Properties panel ↔ Canvas state synchronization
- History manager ↔ Canvas state reconciliation
E2E Testing Scope and Tool
E2E Testing with Playwright:
- Core user flows:
- Create new whiteboard
- Add and manipulate shapes (rectangle, circle, line)
- Use pen tool for freehand drawing
- Save/load whiteboard sessions
- Collaborative editing (simulated multi-user)
- Export functionality (PNG/SVG/JSON)
Critical User Journeys:
- Whiteboard creation → drawing → export → share
- Multi-user collaborative session setup and editing
- Canvas zoom/pan functionality with touch/mouse
Visual Regression Testing
Implementation:
- Use Percy or Chromatic for visual regression testing
- Test key states: empty canvas, various tools selected, multiple shapes drawn, collaboration mode
- Snapshot testing for toolbar states, tool previews, and canvas rendering
Accessibility Testing Automation
Axe-core Integration:
- Run axe-core tests in CI pipeline for all major components:
- Canvas element with proper ARIA roles
- Toolbar buttons with accessible labels
- Keyboard navigation support validation
- Ensure WCAG 2.1 AA compliance for all interactive elements
Test Data Strategy and Fixtures
Test Data Design:
- Mock whiteboard state with various shapes and properties
- Multi-user session fixtures with different user roles (owner, editor, viewer)
- Canvas size and zoom level variations
- Edge case scenarios:
- Empty canvas state
- Large canvas with many shapes
- Complex multi-shape interactions
Fixture Structure:
// test/fixtures/whiteboard-fixtures.ts
interface WhiteboardFixture {
id: string;
name: string;
shapes: Shape[];
tools: Tool[];
collaborators: Collaborator[];
history: HistoryState[];
}
Test Data Pipeline:
- Generate test data programmatically to ensure consistency
- Use factories for creating valid whiteboard states
- Include edge case data (empty, large, malformed) in test suite
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The whiteboard application must meet WCAG 2.1 AA standards across all interactive elements and content:
- Text Contrast: All text must have a minimum 4.5:1 contrast ratio against its background (including UI controls, tool labels, and canvas elements)
- Keyboard Navigation: Full functionality must be operable via keyboard alone (Tab navigation, keyboard shortcuts for tools)
- Focus Management: Visual focus indicators must be clearly visible and follow logical navigation order
- Screen Reader Support: All interactive elements must have proper ARIA roles and labels for screen reader users
- Resize Text: Content must remain usable when text is scaled up to 200%
- No Keyboard Traps: Users must be able to navigate away from any interactive element using only keyboard controls
- Sufficient Touch Target Size: Interactive elements must be at least 44px in both dimensions for touch targets
Keyboard navigation support
The whiteboard must support full keyboard interaction:
- Tab Navigation: Logical tab order through tools, canvas, and UI controls
- Tool Selection: Keyboard shortcuts for each tool (e.g., 'P' for pencil, 'R' for rectangle)
- Canvas Navigation: Arrow keys to pan canvas, Ctrl/Cmd + +/- to zoom
- Selection Management: Delete key to remove selected elements, Ctrl/Cmd + Z for undo
- Context Menus: Enter key to activate context menus, Escape to close them
Focus management strategy
Focus management will be implemented with:
- Focus Trapping: When modal dialogs open, focus is trapped within the dialog until closed
- Focus Restoration: After modals close, focus returns to the previously active element
- Visual Indicators: Clear, high-contrast focus rings that are visible on all interactive elements
- Focus Order Logic: Tab order follows the visual layout of the UI, prioritizing tools → canvas → controls
// Focus management utility for whiteboard tools
const useWhiteboardFocus = () => {
const [activeTool, setActiveTool] = useState<string>('select');
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const handleToolSelect = (tool: string) => {
setActiveTool(tool);
// Ensure focus moves to the canvas after tool selection
setTimeout(() => {
const canvas = document.getElementById('whiteboard-canvas');
if (canvas) canvas.focus();
}, 0);
};
const handleModalOpen = () => {
setIsModalOpen(true);
// Store current focus before opening modal
const previousFocus = document.activeElement;
// Focus trapped within modal - this is handled by React ARIA
};
return {
activeTool,
isModalOpen,
handleToolSelect,
handleModalOpen
};
};
ARIA roles and attributes plan
ARIA implementation will ensure semantic accessibility:
- Canvas:
role="application"witharia-label="Whiteboard canvas" - Tools Panel:
role="toolbar"witharia-label="Drawing tools" - Tool Buttons:
role="button"witharia-pressedfor active state,aria-labelwith tool names - Canvas Elements:
role="img"for shape elements,aria-describedbyfor element descriptions - Modals:
role="dialog"witharia-modal="true",aria-labelledbyfor title - Zoom Controls:
role="slider"witharia-valuenow,aria-valuemin,aria-valuemax
Screen reader compatibility
Screen reader support will include:
- Descriptive Tool Names: Each tool button has meaningful
aria-label(e.g., "Pencil tool") - Element Descriptions: Canvas elements have descriptive
aria-describedbytext - Status Updates: Real-time feedback for actions like "Shape added", "Undo available"
- Live Regions:
aria-live="polite"for status messages and notifications - Keyboard Instructions: Clear instructions displayed when keyboard shortcuts are active
// ARIA-compliant tool button component
interface ToolButtonProps {
id: string;
label: string;
isActive?: boolean;
onClick: () => void;
shortcut?: string;
}
const ToolButton: React.FC<ToolButtonProps> = ({
id,
label,
isActive = false,
onClick,
shortcut
}) => {
const ariaLabel = `${label}${shortcut ? ` (${shortcut})` : ''}`;
return (
<button
id={id}
role="button"
aria-label={ariaLabel}
aria-pressed={isActive}
onClick={onClick}
className={`tool-button ${isActive ? 'active' : ''}`}
tabIndex={0}
>
{label}
{shortcut && (
<span className="shortcut-key" aria-hidden="true">
{shortcut}
</span>
)}
</button>
);
};
Keyboard navigation implementation
The whiteboard will implement a comprehensive keyboard navigation system:
// Keyboard navigation handler for whiteboard
const useWhiteboardKeyboardNavigation = () => {
const canvasRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Tool shortcuts
if (e.key === 'p' || e.key === 'P') {
// Select pencil tool
setActiveTool('pencil');
} else if (e.key === 'r' || e.key === 'R') {
// Select rectangle tool
setActiveTool('rectangle');
} else if (e.key === 'e' || e.e === 'E') {
// Select eraser tool
setActiveTool('eraser');
}
// Canvas navigation
if (e.key === 'ArrowUp' || e.key === 'ArrowDown' ||
e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
handleCanvasPan(e.key);
}
// Zoom controls
if (e.ctrlKey && e.key === '+') {
handleZoomIn();
} else if (e.ctrlKey && e.key === '-') {
handleZoomOut();
}
// Undo/Redo
if (e.ctrlKey && e.key === 'z') {
handleUndo();
} else if (e.ctrlKey && e.key === 'y') {
handleRedo();
}
};
const canvas = canvasRef.current;
if (canvas) {
canvas.addEventListener('keydown', handleKeyDown);
return () => canvas.removeEventListener('keydown', handleKeyDown);
}
}, []);
return { canvasRef };
};
Focus trapping for modals
// Focus trap implementation for whiteboard modals
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLDivElement>) => {
useEffect(() => {
if (!isOpen) return;
const handleFocusTrap = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const focusableElements = modalRef.current?.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || [];
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleFocusTrap);
return () => document.removeEventListener('keydown', handleFocusTrap);
}, [isOpen]);
};
Live region updates
// Status update handler for screen readers
const useStatusUpdates = () => {
const [statusMessage, setStatusMessage] = useState<string>('');
const announceStatus = (message: string) => {
setStatusMessage(message);
// Clear after delay to prevent screen reader spam
setTimeout(() => {
setStatusMessage('');
}, 3000);
};
return {
statusMessage,
announceStatus
};
};
The whiteboard application will provide a fully accessible experience for users with disabilities, ensuring that all functionality is available through keyboard navigation, screen readers, and appropriate ARIA attributes. The design prioritizes inclusive interaction patterns that meet WCAG 2.1 AA standards while maintaining the rich, creative experience expected from a whiteboard application.
10. Security Considerations
XSS Prevention Strategy
- Input sanitization: All user-generated content (text annotations, comments) must be sanitized using a robust HTML sanitizer like DOMPurify before rendering
- Content Security Policy (CSP): Implement strict CSP headers to prevent inline script execution and restrict external resource loading
- Sanitized rendering: Use React's
dangerouslySetInnerHTMLonly with pre-sanitized content, and never directly from user input - Attribute validation: All DOM attributes must be validated against a whitelist of allowed values to prevent malicious attribute injection
CSRF Protection
- CSRF tokens: Implement server-side CSRF token generation and validation for all state-modifying operations (drawing, deleting, sharing)
- Token embedding: Include CSRF tokens in both request headers and form data for all write operations
- Token rotation: Rotate CSRF tokens periodically to prevent token reuse attacks
- SameSite cookies: Set SameSite=Strict for session cookies to mitigate cross-site request forgery
Content Security Policy (CSP) Headers
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
connect-src 'self' wss://;
font-src 'self' data:;
frame-ancestors 'none';
Sensitive Data Handling in Frontend State
- No sensitive data in client-side state: Never store authentication tokens, session IDs, or user credentials in React state or local storage
- Encrypted storage: If storing whiteboard data locally, use encryption (e.g., AES-GCM) with keys derived from user password
- Memory cleanup: Ensure sensitive data is properly cleared from memory when no longer needed
- Secure storage patterns: Use secure HTTP-only cookies for session management instead of localStorage
Input Sanitization Approach
// Example input sanitization for whiteboard text elements
const sanitizeText = (input: string): string => {
// Remove potentially dangerous tags and attributes
const clean = DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'u', 'br'],
ALLOWED_ATTR: [],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed']
});
// Limit text length to prevent abuse
return clean.length > 1000 ? clean.substring(0, 1000) : clean;
};
Token Management Strategy
- JWT validation: All JWT tokens must be validated server-side for expiration and signature integrity before processing
- Token refresh: Implement automatic token refresh with proper error handling for expired tokens
- Secure transport: All authentication tokens must be transmitted over HTTPS only with secure flags set
Real-time Collaboration Security
- Message validation: All WebSocket messages must be validated for proper format and permissions before processing
- Rate limiting: Implement rate limiting on real-time operations to prevent abuse (e.g., 100 ops/minute per user)
- Permission verification: Verify user permissions for each collaborative action before applying changes
- Audit logging: Log all collaborative actions with timestamps and user identifiers for security auditing
Data Validation Strategy
- Schema validation: Validate all incoming data against strict JSON schemas before processing
- Size limits: Implement size limits on uploaded assets and drawing data to prevent DoS attacks
- Type checking: Use TypeScript strict mode to enforce type safety throughout the application
- Range validation: Validate coordinate ranges and size constraints for drawing operations
Secure WebSocket Connections
- WSS protocol: All real-time communication must use WSS (WebSocket Secure) instead of WS
- Authentication handshake: Implement proper authentication before establishing real-time connections
- Message encryption: Use TLS 1.3+ for all WebSocket traffic to ensure end-to-end encryption
- Connection limits: Implement per-user connection limits to prevent resource exhaustion
Third-party Library Security
- Dependency scanning: Regularly scan dependencies for known vulnerabilities using tools like npm audit or Snyk
- Version pinning: Pin dependency versions to prevent automatic upgrades of vulnerable packages
- Minimal permissions: Only include libraries that are absolutely necessary for core functionality
- Security patches: Establish process for quickly applying security patches to third-party dependencies
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following BEM-style naming with feature-scoped keys:
// Translation keys for whiteboard feature
{
"whiteboard": {
"toolbar": {
"pen": "Pen",
"eraser": "Eraser",
"text": "Text",
"shape": "Shape",
"select": "Select",
"undo": "Undo",
"redo": "Redo",
"clear": "Clear Board"
},
"contextMenu": {
"copy": "Copy",
"cut": "Cut",
"paste": "Paste",
"delete": "Delete",
"duplicate": "Duplicate"
},
"modal": {
"title": "Whiteboard",
"save": "Save",
"cancel": "Cancel",
"saveSuccess": "Whiteboard saved successfully",
"saveError": "Failed to save whiteboard"
},
"shape": {
"rectangle": "Rectangle",
"circle": "Circle",
"triangle": "Triangle"
},
"tool": {
"strokeWidth": "Stroke Width",
"fillColor": "Fill Color",
"strokeColor": "Stroke Color"
}
}
}
RTL Support Considerations
RTL layout handling through CSS classes and direction properties:
/* CSS for RTL support */
[dir="rtl"] .whiteboard-toolbar {
flex-direction: row-reverse;
}
[dir="rtl"] .whiteboard-canvas {
transform: scaleX(-1);
}
Date, Number, Currency Formatting Strategy
Using Intl API for locale-aware formatting:
// Format date with user's locale preference
const formatDate = (date: Date, locale: string) => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
// Format stroke width with locale-aware numbers
const formatStrokeWidth = (value: number, locale: string) => {
return new Intl.NumberFormat(locale, {
maximumFractionDigits: 1
}).format(value);
};
Locale Detection and Switching Strategy
Automatic detection with manual override:
// Detect user's preferred language from browser or system settings
const detectLocale = (): string => {
const storedLocale = localStorage.getItem('preferredLocale');
if (storedLocale) return storedLocale;
const browserLang = navigator.language || navigator.languages[0];
const supportedLocales = ['en', 'es', 'fr', 'de', 'ja'];
// Match to supported locales or default to English
return supportedLocales.find(locale =>
browserLang.startsWith(locale) || locale === browserLang
) || 'en';
};
// Locale switcher component
const LocaleSwitcher = () => {
const [currentLocale, setCurrentLocale] = useState<string>(detectLocale());
const handleLocaleChange = (newLocale: string) => {
setCurrentLocale(newLocale);
localStorage.setItem('preferredLocale', newLocale);
// Update document direction for RTL support
document.documentElement.dir =
['ar', 'he', 'fa'].includes(newLocale) ? 'rtl' : 'ltr';
};
return (
<select
value={currentLocale}
onChange={(e) => handleLocaleChange(e.target.value)}
>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ja">日本語</option>
</select>
);
};
Translation Implementation Pattern
Using react-i18next with namespace support:
// i18n configuration setup
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-middleware';
i18n
.use(Backend)
.use(initReactI18next)
.init({
lng: detectLocale(),
fallbackLng: 'en',
ns: ['whiteboard', 'common'],
defaultNS: 'whiteboard',
interpolation: {
escapeValue: false
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
}
});
// Translation hook for whiteboard feature
const useWhiteboardTranslation = () => {
const { t, i18n } = useTranslation('whiteboard');
return {
t,
currentLocale: i18n.language,
changeLanguage: i18n.changeLanguage
};
};
Language-Specific Character Handling
Support for various text input systems:
// Handle multilingual text input with proper character encoding
const handleTextInput = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = event.target.value;
// Validate and sanitize input based on language context
if (isRTLLanguage(i18n.language)) {
// Apply RTL-specific text handling
event.target.style.direction = 'rtl';
}
return text;
};
// Language-specific character validation
const validateCharacter = (char: string, locale: string): boolean => {
const arabicChars = /[\u0600-\u06FF]/;
const hebrewChars = /[\u0590-\u05FF]/;
switch (locale) {
case 'ar':
return arabicChars.test(char);
case 'he':
return hebrewChars.test(char);
default:
return true;
}
};
Translation Key Management
Using a centralized key registry to prevent duplication:
// Translation keys registry (auto-generated from code comments)
const WHITEBOARD_TRANSLATION_KEYS = {
toolbar: {
pen: 'whiteboard.toolbar.pen',
eraser: 'whiteboard.toolbar.eraser',
text: 'whiteboard.toolbar.text',
shape: 'whiteboard.toolbar.shape',
select: 'whiteboard.toolbar.select',
undo: 'whiteboard.toolbar.undo',
redo: 'whiteboard.toolbar.redo',
clear: 'whiteboard.toolbar.clear'
},
contextMenu: {
copy: 'whiteboard.contextMenu.copy',
cut: 'whiteboard.contextMenu.cut',
paste: 'whiteboard.contextMenu.paste',
delete: 'whiteboard.contextMenu.delete',
duplicate: 'whiteboard.contextMenu.duplicate'
}
} as const;
type WhiteboardTranslationKeys = typeof WHITEBOARD_TRANSLATION_KEYS;
Testing Internationalization
Testing with multiple locales:
// Test case for RTL layout
test('RTL layout applies correctly', () => {
const { container } = renderWithLocale(<Whiteboard />, 'ar');
expect(container.firstChild).toHaveStyle({
direction: 'rtl'
});
});
// Test case for date formatting
test('date formats correctly in different locales', () => {
const { t } = useWhiteboardTranslation();
const date = new Date('2023-12-25');
const formattedDate = formatDate(date, 'fr');
expect(formattedDate).toContain('déc');
});
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The whiteboard application should track key performance indicators to ensure optimal user experience:
- LCP (Largest Contentful Paint): Target <2.5s for canvas rendering and element loading
- FID (First Input Delay): Target <100ms for pen/stylus input responsiveness
- CLS (Cumulative Layout Shift): Target <0.1 for smooth canvas interactions
- FCP (First Contentful Paint): Target <1.5s for initial whiteboard load
- Session Duration: Track average session length to understand user engagement
- Active Drawing Time: Measure time spent actively creating content vs. idle time
Error Tracking Integration
Sentry Implementation Strategy:
// Error tracking integration with Sentry
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
environment: process.env.REACT_APP_ENVIRONMENT,
release: `whiteboard-app@${process.env.REACT_APP_VERSION}`,
beforeSend: (event, hint) => {
// Filter out known non-critical errors
if (hint?.originalException instanceof Error &&
hint.originalException.message.includes('canvas')) {
return event;
}
return null; // Drop non-critical canvas errors
}
});
Custom Events and User Journey Tracking
Whiteboard Interaction Events:
// Custom event tracking for whiteboard interactions
const trackWhiteboardEvent = (event: string, payload?: any) => {
// Track user actions with context
Sentry.captureEvent({
event_id: uuidv4(),
type: 'transaction',
transaction: `whiteboard.${event}`,
timestamp: Date.now(),
contexts: {
user: { id: getCurrentUserId() },
whiteboard: {
id: getActiveWhiteboardId(),
elements: getElementCount(),
collaborators: getCollaboratorCount()
}
},
measurements: {
canvas_width: getCanvasWidth(),
canvas_height: getCanvasHeight()
}
});
};
// Example usage
trackWhiteboardEvent('draw.start', { tool: 'pen', color: '#ff0000' });
trackWhiteboardEvent('element.add', { type: 'rectangle', width: 100 });
Performance Monitoring and Regression Detection
Performance Baseline Tracking:
// Performance monitoring with baseline comparisons
class WhiteboardPerformanceMonitor {
private baselineMetrics = new Map<string, number>();
constructor() {
this.initializeBaselines();
}
private initializeBaselines() {
// Set baseline performance targets
this.baselineMetrics.set('canvas_render_time', 150); // ms
this.baselineMetrics.set('pen_input_delay', 50); // ms
this.baselineMetrics.set('sync_latency', 200); // ms
}
trackPerformanceMetric(name: string, value: number) {
const baseline = this.baselineMetrics.get(name);
if (baseline && value > baseline * 1.5) {
Sentry.captureMessage(`Performance regression detected: ${name}`, {
level: 'warning',
extra: {
current: value,
baseline: baseline,
threshold: baseline * 1.5
}
});
}
}
}
Real-time Canvas Performance Monitoring:
// Canvas rendering performance monitoring
const useCanvasPerformance = () => {
const [renderStats, setRenderStats] = useState({
frameRate: 60,
renderTime: 0,
elementCount: 0
});
useEffect(() => {
const frameTimer = setInterval(() => {
const startTime = performance.now();
// Simulate canvas rendering
requestAnimationFrame(() => {
const endTime = performance.now();
const renderTime = endTime - startTime;
setRenderStats(prev => ({
...prev,
renderTime,
frameRate: Math.min(60, Math.floor(1000 / renderTime))
}));
// Alert on performance degradation
if (renderTime > 100) {
Sentry.captureMessage('Canvas rendering performance degraded', {
level: 'warning',
extra: { renderTime, frameRate: 60 }
});
}
});
}, 1000);
return () => clearInterval(frameTimer);
}, []);
return renderStats;
};
Integration with Observability Platforms
Datadog APM Integration:
// Datadog RUM integration for whiteboard sessions
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: process.env.REACT_APP_DATADOG_APPLICATION_ID,
clientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN,
site: 'datadoghq.com',
service: 'whiteboard-app',
env: process.env.REACT_APP_ENVIRONMENT,
version: process.env.REACT_APP_VERSION,
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask-user-input'
});
Custom Dashboard Metrics:
- Active Whiteboards: Real-time count of concurrent whiteboard sessions
- Collaboration Events: Number of real-time updates per second
- Canvas Sync Latency: Average time between drawing and sync completion
- User Session Success Rate: Percentage of successful whiteboard creation/load operations
Alerting Strategy
Performance Thresholds:
// Performance alert thresholds configuration
const PERFORMANCE_ALERTS = {
canvas_render_time: {
warning: 150, // ms
critical: 300 // ms
},
sync_latency: {
warning: 200, // ms
critical: 500 // ms
},
input_delay: {
warning: 100, // ms
critical: 250 // ms
}
};
// Alert trigger function
const checkPerformanceAlerts = (metrics: any) => {
Object.entries(PERFORMANCE_ALERTS).forEach(([metric, thresholds]) => {
if (metrics[metric] > thresholds.critical) {
// Send critical alert to monitoring system
sendAlert({
severity: 'critical',
metric,
value: metrics[metric],
threshold: thresholds.critical
});
} else if (metrics[metric] > thresholds.warning) {
// Send warning alert
sendAlert({
severity: 'warning',
metric,
value: metrics[metric],
threshold: thresholds.warning
});
}
});
};
Data Collection and Privacy
Privacy-First Approach:
// Privacy-preserving telemetry configuration
const telemetryConfig = {
// Disable tracking of user content or identifiers
trackUserContent: false,
trackUserIds: false,
trackCanvasContent: false,
// Enable only performance and interaction data
trackPerformance: true,
trackUserInteractions: true,
trackErrors: true,
// Anonymize user sessions
anonymizeUserSessions: true,
sessionSamplingRate: 0.1 // 10% of sessions
};
Observability Roadmap
Phase 1 - Core Metrics: Basic RUM and error tracking
Phase 2 - Advanced Analytics: User journey mapping and collaboration analytics
Phase 3 - Predictive Monitoring: Performance prediction and proactive alerts
The monitoring strategy focuses on real-time feedback for performance optimization while maintaining user privacy and ensuring minimal impact on application performance.
13. File & Folder Structure
Proposed Directory Structure
/src
/components
/whiteboard
Whiteboard.tsx
Toolbar.tsx
Canvas.tsx
ShapeTool.tsx
TextTool.tsx
ToolPicker.tsx
UndoRedoButtons.tsx
/ui
Button.tsx
Input.tsx
Modal.tsx
Tooltip.tsx
/common
Icons.tsx
ThemeProvider.tsx
/features
/whiteboard
index.ts
types.ts
hooks/
useWhiteboard.ts
useCanvas.ts
store/
whiteboardSlice.ts
utils/
shapeUtils.ts
drawingUtils.ts
/hooks
useDebounce.ts
useResizeObserver.ts
/services
api/
whiteboardApi.ts
types.ts
storage/
localStorageService.ts
/types
index.ts
whiteboard.ts
/utils
domUtils.ts
eventUtils.ts
validation.ts
/styles
globals.css
theme.ts
Module Boundaries and Import Rules
- Feature isolation: Whiteboard feature is completely self-contained with no cross-feature imports
- Shared components: UI primitives like Button, Modal live in
/components/uiand are imported by whiteboard components - Barrel exports: Each feature has an
index.tsthat re-exports public APIs for clean imports - Type safety: All feature-specific types are contained within their respective folders
Shared vs Feature-Specific Code Separation
Shared code (reusable across features):
- UI primitives (
Button,Input) - Common utilities (
domUtils,eventUtils) - Global styles and theme providers
- Common hooks like
useDebounce
Feature-specific code:
- Whiteboard-specific components (
Canvas,Toolbar) - Whiteboard business logic and state management
- API service layer for whiteboard operations
- Shape-specific drawing utilities
This structure follows a feature-based organization that scales well with team growth, where each feature can be developed and tested independently while maintaining clear boundaries between modules.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React + ReactDOM
- Justification: Core framework for building the UI with component-based architecture and hooks
- Version: ^18.2.0 (latest stable)
- Bundle size impact: Minimal, required for all React applications
2. TypeScript
- Justification: Essential for type safety in large-scale frontend applications, catching runtime errors at compile time
- Version: ^5.0.0
- Bundle size impact: None (compile-time only)
3. React Router DOM
- Justification: Declarative routing for navigation between whiteboard pages and shared boards
- Version: ^6.14.0
- Bundle size impact: ~20KB (minimal overhead)
4. Zustand
- Justification: Lightweight state management with minimal boilerplate for whiteboard session state, user preferences, and collaborative data
- Version: ^4.3.0
- Bundle size impact: ~2KB (very lightweight)
5. React Query
- Justification: Production-ready data fetching and caching for whiteboard content, user sessions, and collaboration state
- Version: ^5.17.0
- Bundle size impact: ~15KB (essential for real-time features)
6. Tailwind CSS + PostCSS
- Justification: Utility-first CSS framework for rapid UI development and consistent design system
- Version: ^3.3.0 (Tailwind), ^8.4.0 (PostCSS)
- Bundle size impact: ~50KB (bundled with build process)
7. Framer Motion
- Justification: Smooth animations for drawing tools, transitions, and collaborative user indicators
- Version: ^10.12.0
- Bundle size impact: ~15KB (optional but enhances UX)
8. react-dnd
- Justification: Drag-and-drop functionality for moving and resizing whiteboard elements
- Version: ^16.0.0
- Bundle size impact: ~35KB (required for core interaction)
9. react-resizable
- Justification: Resizable whiteboard elements (text boxes, shapes) with consistent UX
- Version: ^3.0.0
- Bundle size impact: ~5KB
10. react-colorful
- Justification: Color picker component with excellent accessibility and consistent design language
- Version: ^1.5.0
- Bundle size impact: ~2KB
11. @heroicons/react
- Justification: Clean, accessible icon set for toolbar and UI elements with consistent design language
- Version: ^2.0.0
- Bundle size impact: ~1KB
12. jotai
- Justification: Alternative atom-based state management for granular whiteboard element state (optional but useful)
- Version: ^2.4.0
- Bundle size impact: ~5KB (optional dependency)
Alternatives Considered and Rejected
1. Redux Toolkit vs Zustand
- Rejected: Zustand chosen for simpler setup and better developer experience in whiteboard context
- Reason: Redux Toolkit adds significant boilerplate for a feature that benefits more from simple state management
2. Material UI vs Tailwind CSS
- Rejected: Tailwind chosen for faster development and better customization control
- Reason: MUI's component library would require extensive overrides to match whiteboard design requirements
3. Fabric.js vs react-konva
- Rejected: react-konva chosen for better React integration and performance in whiteboard context
- Reason: Fabric.js has more complex API surface for simple whiteboard use cases
4. Socket.IO vs WebSockets
- Rejected: React Query with real-time updates chosen for better state management and caching
- Reason: Direct WebSocket implementation would require more boilerplate and error handling
Licensing and Bundle Size Impact Assessment
| Library | License | Bundle Size | Notes |
|---|---|---|---|
| React + ReactDOM | MIT | ~100KB | Core dependencies |
| TypeScript | Apache 2.0 | None | Compile-time only |
| React Router DOM | MIT | ~20KB | Essential for routing |
| Zustand | MIT | ~2KB | Minimal overhead |
| React Query | MIT | ~15KB | Critical for real-time |
| Tailwind CSS | MIT | ~50KB | Build-time optimization |
| Framer Motion | MIT | ~15KB | Optional but recommended |
| react-dnd | MIT | ~35KB | Required for drag/drop |
| react-resizable | MIT | ~5KB | Lightweight |
| react-colorful | MIT | ~2KB | Simple color picker |
| @heroicons/react | MIT | ~1KB | Icon set |
| jotai | MIT | ~5KB | Optional |
Bundle Size Budget Allocation
- Core Framework: 120KB (React, ReactDOM, Router)
- State Management: 20KB (Zustand + React Query)
- UI Components: 100KB (Tailwind, Icons, Motion)
- Interaction Features: 45KB (DnD, Resizable)
- Utility Libraries: 10KB (Color picker, etc.)
- Total Estimated: ~295KB (excluding dev dependencies)
Performance Considerations
- All libraries are selected with tree-shaking support
- Bundle size is optimized through module resolution and build-time optimizations
- Critical path dependencies are prioritized for faster load times
- Optional libraries can be lazy-loaded to reduce initial bundle size
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
Real-time collaboration model: Should we implement operational transformation (OT) or conflict-free replicated data types (CRDTs) for concurrent editing? OT provides better consistency but is complex to implement, while CRDTs are simpler but may have less predictable behavior for complex operations.
Drawing tools complexity: How granular should the drawing tool selection be? Should we support individual brush sizes, opacity controls, and color pickers, or start with a simplified set of tools that can be expanded later?
Storage strategy: Should whiteboard content be persisted in real-time to a database or cached locally and synced periodically? Real-time persistence increases server load but provides better user experience.
Known trade-offs made in this design
Performance vs. Feature Richness: Prioritizing smooth canvas interactions over extensive toolset features, particularly for touch devices where complex UI elements might impact performance. We're trading rich feature sets for optimal rendering and responsiveness.
State Management Simplicity vs. Scalability: Using React Context API for state management rather than Redux Toolkit or Zustand to reduce initial complexity and setup overhead. This trade-off makes the system less extensible for complex multi-user scenarios.
Client-side rendering vs. SSR: Choosing CSR over SSR for whiteboard content to enable real-time collaborative editing and reduce server-side complexity. This means the whiteboard won't be immediately visible to users without JavaScript.
Risks and mitigations
Canvas rendering performance: Large whiteboards with many elements may cause performance degradation. Mitigation includes implementing virtualization for large element lists and optimizing canvas rendering through batching and requestAnimationFrame.
Network reliability in collaborative mode: Real-time collaboration depends heavily on stable network connections. Mitigation includes implementing optimistic updates with local state reconciliation and providing clear error states for disconnected users.
Memory leaks in collaborative sessions: Long-running whiteboard sessions may accumulate memory from event listeners and cached data. Mitigation includes implementing proper cleanup in useEffect and useCallback dependencies, and setting up memory monitoring.
Technical debt acknowledged
Limited accessibility support in early implementation: The initial design focuses on core functionality and doesn't fully implement ARIA roles or keyboard navigation patterns. This creates technical debt that must be addressed in subsequent iterations.
Simplified state normalization: The current state management approach doesn't implement proper data normalization for large whiteboard content, which could lead to performance issues as boards grow in complexity.
Basic error handling: The error boundaries and global error handling are minimal, focusing only on critical failures. More comprehensive error categorization and user feedback systems will need to be built out.
No offline-first capability: The design assumes consistent network connectivity, which means users lose access to whiteboard content when offline. Implementing proper offline support would require significant additional complexity and storage strategy design.
Limited collaboration state management: The current approach doesn't handle complex user presence states or conflict resolution strategies, which could become problematic in high-concurrency scenarios.