This document describes the implementation of the Presentation Mode feature in AstraDraw, inspired by the Obsidian Excalidraw plugin's slideshow functionality.
Presentation Mode allows users to create presentations using Excalidraw frames as slides. Each frame becomes a slide that can be navigated through in fullscreen mode with smooth animations.
- Header: "Presentation" with a "+ Create slide" button that activates the Frame tool
- Slide List: Scrollable list of slide thumbnails with frame names
- Reordering: β/β buttons on each slide card to change presentation order
- Start Button: "Start presentation" button at the bottom
- Empty State: When no frames exist, shows instructions: "Create presentations with AstraDraw"
- Navigation: Left/Right arrow buttons
- Slide Indicator: "Slide X/Y" counter
- Laser Pointer: Toggle laser drawing tool (wand icon)
- Dark Mode: Toggle dark/light theme (moon/sun icon)
- Fullscreen: Toggle fullscreen mode
- End Presentation: Red button to exit presentation mode
| Key | Action |
|---|---|
β / β / Space |
Next slide |
β / β |
Previous slide |
Escape |
End presentation |
F |
Toggle fullscreen |
L |
Toggle laser pointer |
frontend/excalidraw-app/components/Presentation/
βββ index.ts # Exports
βββ usePresentationMode.ts # Core logic hook (Jotai state)
βββ PresentationPanel.tsx # Sidebar panel component
βββ PresentationPanel.scss # Sidebar panel styles
βββ PresentationMode.tsx # Portal wrapper for controls
βββ PresentationControls.tsx # Bottom control bar component
βββ PresentationControls.scss # Control bar styles
// Core presentation state atoms
isPresentationModeAtom // boolean - is presentation active
currentSlideAtom // number - current slide index
slidesAtom // ExcalidrawFrameLikeElement[] - ordered frames
isLaserActiveAtom // boolean - laser tool state
originalThemeAtom // Theme | null - theme before presentationMain logic hook that provides:
isPresentationMode- current mode statecurrentSlide/slides- navigation stateisLaserActive- laser tool stategetFrames()- get all frame elements from canvasstartPresentation()- enter presentation modeendPresentation()- exit presentation modenextSlide()/prevSlide()/goToSlide()- navigationtoggleLaser()/toggleTheme()/toggleFullscreen()- togglessetSlides()- set custom slide order
Sidebar panel that:
- Fetches frames using
excalidrawAPI.getSceneElements() - Maintains local
orderedFramesstate for reordering - Syncs with canvas (adds new frames, removes deleted ones)
- Renders
SlideThumbcomponents with move up/down buttons - Calls
setSlides(orderedFrames)before starting presentation
Bottom control bar that:
- Renders navigation, laser, theme, fullscreen, and end buttons
- Auto-fades after 3 seconds of inactivity
- Shows on mouse movement
- Uses localized strings via
t()function
Portal wrapper that:
- Renders
PresentationControlsintodocument.body - Only renders when
isPresentationModeis true
- Both sidebars close automatically:
- Left workspace sidebar (via
closeWorkspaceSidebarAtom) - explicitly closed, not toggled - Right default sidebar (via
excalidrawAPI.toggleSidebar)
- Left workspace sidebar (via
- View mode and Zen mode are enabled (hides UI)
- Pen toolbar is hidden
- Laser tool is activated by default
- First slide is displayed with smooth animation (800ms)
- Keyboard listeners are attached
Why close instead of toggle? The left workspace sidebar is explicitly closed (not toggled) because:
- We need to guarantee the canvas is at full width for optimal slide display
- If we toggled and the sidebar was already closed, it would open (reducing canvas space)
- Using
closeWorkspaceSidebarAtomensures consistent behavior regardless of sidebar's current state
- Uses
excalidrawAPI.scrollToContent()with:fitToContent: true- frame fills viewportanimate: true- smooth transitionduration: 800- animation duration in ms
- Uses Excalidraw's built-in laser tool
- Activated via
excalidrawAPI.setActiveTool({ type: "laser" }) - Drawings fade out automatically (laser behavior)
- Original theme is restored
- View mode and Zen mode are disabled
- Laser tool is deactivated
- Fullscreen is exited (if active)
- Keyboard listeners are removed
- UI elements become visible again
- Fixed position at bottom center
- Rounded corners with shadow
- Dark mode support via CSS variables
- Fade animation on inactivity:
- Fade out: 0.8s ease-out to 0.1 opacity
- Fade in: 0.2s ease-in on hover
- Slide thumbnails with frame names
- Reorder buttons (β/β) on hover
- Empty state with instructions
- Dark mode support
All UI strings are localized. Keys added to locales/en.json and locales/ru-RU.json:
{
"presentation.title": "Presentation",
"presentation.createSlide": "+ Create slide",
"presentation.startPresentation": "Start presentation",
"presentation.endPresentation": "End presentation",
"presentation.slideCounter": "Slide {{current}}/{{total}}",
"presentation.previousSlide": "Previous slide",
"presentation.nextSlide": "Next slide",
"presentation.toggleLaser": "Toggle laser pointer",
"presentation.toggleTheme": "Toggle dark mode",
"presentation.toggleFullscreen": "Toggle fullscreen",
"presentation.emptyStateTitle": "Create presentations with AstraDraw",
"presentation.emptyStateDescription": "Use the Frame tool to create slides for your presentation",
"presentation.moveUp": "Move up",
"presentation.moveDown": "Move down"
}| File | Changes |
|---|---|
frontend/excalidraw-app/App.tsx |
Import and render PresentationMode component |
frontend/excalidraw-app/components/AppSidebar.tsx |
Import and render PresentationPanel, pass excalidrawAPI |
frontend/excalidraw-app/pens/PenToolbar.tsx |
Hide when zenModeEnabled or viewModeEnabled |
frontend/packages/excalidraw/locales/en.json |
Add presentation translation keys |
frontend/packages/excalidraw/locales/ru-RU.json |
Add Russian translations |
// Navigation
excalidrawAPI.scrollToContent(frame, { fitToContent: true, animate: true, duration: 800 })
// Tool switching
excalidrawAPI.setActiveTool({ type: "laser" })
excalidrawAPI.setActiveTool({ type: "selection" })
// Scene access
excalidrawAPI.getSceneElements()
excalidrawAPI.getAppState()
// UI control
excalidrawAPI.updateScene({ appState: { viewModeEnabled, zenModeEnabled } })
excalidrawAPI.toggleSidebar({ name: "default", force: false }) // Right sidebar
// Workspace sidebar (left) - via Jotai atom
import { useSetAtom } from "../../app-jotai";
import { closeWorkspaceSidebarAtom } from "../Settings/settingsState";
const closeWorkspaceSidebar = useSetAtom(closeWorkspaceSidebarAtom);
closeWorkspaceSidebar();- Drag-and-drop slide reordering (currently uses β/β buttons)
- Slide preview on hover
- Presenter notes
- Slide timing/auto-advance
- Export to PDF/images
- Remote control support
| Date | Changes |
|---|---|
| 2025-12-23 | Added auto-close of left workspace sidebar via closeWorkspaceSidebarAtom |
| 2025-12-23 | Documented why close (not toggle) is used for sidebar control |