Integrating Excalidraw: Building an Online Diagramming Tool#

I needed an online diagramming tool recently. After evaluating several open-source options, I chose Excalidraw. It’s free, open-source, powerful, and smooth. Today I’ll share my experience integrating Excalidraw into a Next.js project.

Why Excalidraw#

Excalidraw is an open-source virtual whiteboard with key features:

  • Hand-drawn style: Lines have a subtle “wiggle”, more approachable
  • Lightweight: Core library is only 280KB gzipped
  • Feature-rich: Rectangles, arrows, text, freehand drawing
  • Export support: PNG, SVG, JSON formats
  • Open source: MIT license, customize freely

Compared to heavyweight tools like draw.io, Excalidraw is lighter and better for quick architecture diagrams and flowcharts.

Dynamic Import: Solving SSR Issues#

Excalidraw depends on browser APIs (localStorage, canvas), so direct import fails during SSR. Dynamic import is required:

'use client'

import { useState, useEffect } from 'react'
import '@excalidraw/excalidraw/index.css'

export default function DiagramTool() {
  const [Excalidraw, setExcalidraw] = useState<any>(null)
  const [mounted, setMounted] = useState(false)

  // Avoid hydration errors
  useEffect(() => {
    setMounted(true)
  }, [])

  // Dynamic import Excalidraw
  useEffect(() => {
    if (!mounted) return
    
    import('@excalidraw/excalidraw')
      .then((mod) => {
        setExcalidraw(() => mod.Excalidraw)
      })
      .catch((err) => {
        console.error('Failed to load Excalidraw:', err)
      })
  }, [mounted])

  if (!mounted || !Excalidraw) {
    return <LoadingSpinner />
  }

  return <Excalidraw />
}

Key points:

  1. ‘use client’ directive: Required for Next.js 13+ App Router client components
  2. mounted state: Ensure client-side only rendering, avoid SSR/CSR mismatch
  3. Dynamic import: Asynchronous loading prevents SSR errors
  4. CSS import: Must import index.css, or styles will be missing

Local Storage: Auto-save Drawings#

User drawings should auto-save to prevent loss on refresh. Excalidraw provides an onChange callback:

const handleChange = useCallback((elements: any[]) => {
  try {
    localStorage.setItem('excalidraw-elements', JSON.stringify(elements))
  } catch (e) {
    console.error('Failed to save:', e)
  }
}, [])

<Excalidraw onChange={handleChange} />

Loading saved data:

const [initialData, setInitialData] = useState<any>(null)

useEffect(() => {
  try {
    const saved = localStorage.getItem('excalidraw-elements')
    if (saved) {
      setInitialData({
        elements: JSON.parse(saved)
      })
    }
  } catch (e) {
    console.error('Failed to load:', e)
  }
}, [])

<Excalidraw initialData={initialData} />

Note: localStorage has a 5MB limit. Complex diagrams may exceed this. For production, consider IndexedDB or cloud storage.

Fullscreen Mode: Immersive Drawing#

Drawing needs space. Fullscreen is essential:

const [isFullscreen, setIsFullscreen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)

const toggleFullscreen = useCallback(async () => {
  if (!containerRef.current) return
  
  try {
    if (!document.fullscreenElement) {
      await containerRef.current.requestFullscreen()
      setIsFullscreen(true)
    } else {
      await document.exitFullscreen()
      setIsFullscreen(false)
    }
  } catch (err) {
    console.error('Fullscreen error:', err)
  }
}, [])

// Listen for fullscreen changes (user presses ESC)
useEffect(() => {
  const handleFullscreenChange = () => {
    setIsFullscreen(!!document.fullscreenElement)
  }
  document.addEventListener('fullscreenchange', handleFullscreenChange)
  return () => document.removeEventListener('fullscreenchange', handleFullscreenChange)
}, [])

return (
  <div 
    ref={containerRef}
    className={isFullscreen ? 'fixed inset-0 z-50' : 'h-[600px]'}
  >
    <Excalidraw />
    <button onClick={toggleFullscreen}>
      {isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
    </button>
  </div>
)

Key details:

  1. ref for container: Need DOM element to call requestFullscreen
  2. Listen to fullscreenchange: Update state when user presses ESC
  3. fixed inset-0: Cover entire screen when fullscreen

Library: Pre-built Shape Templates#

Excalidraw supports a Library for pre-built shapes. A library is a JSON file:

{
  "type": "excalidrawlib",
  "version": 1,
  "library": [
    [
      {"type": "rectangle", "x": 0, "y": 0, "width": 100, "height": 60},
      {"type": "text", "x": 20, "y": 20, "text": "Server"}
    ]
  ]
}

Loading the library:

const [libraryItems, setLibraryItems] = useState<any[]>([])

useEffect(() => {
  const loadLibrary = async () => {
    try {
      const response = await fetch('/library/system-design.excalidrawlib')
      const libraryData = await response.json()
      
      if (libraryData.library && Array.isArray(libraryData.library)) {
        const items = libraryData.library.map((elements: any[], index: number) => ({
          id: `library-item-${index}`,
          status: 'published',
          elements: elements || [],
          created: Date.now(),
        }))
        setLibraryItems(items)
      }
    } catch (err) {
      console.error('Failed to load library:', err)
    }
  }
  
  loadLibrary()
}, [])

<Excalidraw initialData={{ libraryItems }} />

Libraries are perfect for teams—pre-built architecture templates, common icons, etc.

Theme Support: Dark Mode#

Excalidraw natively supports dark mode via the theme prop:

import { useTheme } from '@/components/theme/ThemeProvider'

export default function DiagramTool() {
  const { theme } = useTheme()
  
  return (
    <Excalidraw
      theme={theme}  // 'light' or 'dark'
      langCode={locale === 'zh' ? 'zh-CN' : 'en'}
    />
  )
}

Performance: Large Canvases#

When canvas elements exceed 1000, lag may occur. Optimization tips:

1. Virtualized Rendering#

Excalidraw internally implements viewport culling, only rendering visible elements. But for huge canvases (10000+ elements), manually limit:

const handleChange = useCallback((elements: any[]) => {
  if (elements.length > 5000) {
    alert('Too many elements. Consider splitting into multiple canvases.')
    return
  }
  // Save logic
}, [])

2. Compressed Storage#

Remove unnecessary properties when saving:

const handleChange = useCallback((elements: any[]) => {
  const compressed = elements.map(el => ({
    id: el.id,
    type: el.type,
    x: el.x,
    y: el.y,
    width: el.width,
    height: el.height,
    strokeColor: el.strokeColor,
    // Keep only essential fields
  }))
  localStorage.setItem('excalidraw-elements', JSON.stringify(compressed))
}, [])

3. Export Optimization#

Specify scale when exporting to PNG:

const exportToPng = async () => {
  const blob = await exportToBlob({
    elements,
    mimeType: 'image/png',
    quality: 0.8,  // Compression quality
    scale: 1       // Scale ratio
  })
  // Download logic
}

Edge Cases#

1. Mobile Support#

Excalidraw has good touch support, but you may want to disable pinch zoom:

<Excalidraw
  UIOptions={{
    canvasActions: {
      zoom: false  // Disable zoom
    }
  }}
/>

2. Import Compatibility#

Importing from older Excalidraw versions may have compatibility issues:

const loadScene = async (data: string) => {
  try {
    const elements = JSON.parse(data)
    // Version check
    if (!Array.isArray(elements)) {
      throw new Error('Invalid format')
    }
    return elements
  } catch (e) {
    alert('Import failed: Invalid file format')
    return []
  }
}

3. Concurrent Editing#

Excalidraw is single-user. Multiple users editing simultaneously causes conflicts. For collaboration:

  • Use Excalidraw’s official collaboration service
  • Implement your own WebSocket sync

The Result#

Based on these ideas, I built: Excalidraw Diagramming Tool

Features:

  • Free online diagramming, no registration
  • Rectangles, arrows, text, freehand drawing
  • Auto-save to local storage
  • Fullscreen immersive mode
  • Export to PNG, SVG, JSON
  • Dark mode support

Integrating Excalidraw isn’t complex. The key is handling dynamic import, local storage, fullscreen, and other details correctly. Hope this helps.


Related: Mermaid Editor | Mindmap