Presenter mode
Presenter mode provides a two-view system: the presenter gets a speaker view with notes, timers, and navigation controls; the audience sees the current slide in sync. Only one presenter can be active at a time.
Starting a presentation
Section titled “Starting a presentation”await presentation.presenter.start({ fromSlide: 0, // slide index (default: 0) countdown: 1800, // 30-minute countdown in seconds (optional)});Full options:
interface StartOptions { fromSlide?: number; countdown?: number; autoAdvance?: boolean; autoAdvanceInterval?: number; // seconds between auto-advances}Calling start() sets presentationState.isPresenting = true in the Y.js document. All connected clients receive the update immediately.
Navigation
Section titled “Navigation”presentation.presenter.next();presentation.presenter.previous();presentation.presenter.first();presentation.presenter.last();presentation.presenter.goToSlide(5); // zero-based indexSlides with hidden: true are skipped automatically during next() and previous().
Suggested keyboard bindings for a custom speaker UI:
| Key | Action |
|---|---|
→ Space Enter | Next slide |
← Backspace | Previous slide |
Home | First slide |
End | Last slide |
L | Toggle laser |
B | Toggle blackout |
Esc | End presentation |
Speaker view
Section titled “Speaker view”The speaker view typically shows: current slide preview, next slide thumbnail, speaker notes, timer, and slide count. Build it from the state returned by subscribe():
presentation.presenter.subscribe(state => { updateSlideCounter(state.currentSlideIndex, state.totalSlides); updateTimer(state); if (state.countdown) { const { remaining } = state.countdown; setTimerColor(remaining > 300 ? 'green' : remaining > 60 ? 'yellow' : 'red'); }});Reading speaker notes
Section titled “Reading speaker notes”Notes are Markdown. Render them in the speaker panel for the current slide:
const currentSlideId = presentation.slides.list()[state.currentSlideIndex]?.id;const notes = presentation.speakerNotes.get(currentSlideId);notesPanel.innerHTML = renderMarkdown(notes.toString());
notes.observe(() => { notesPanel.innerHTML = renderMarkdown(notes.toString());});timer.elapsed(): number // seconds since presentation startedtimer.slideElapsed(): number // seconds on current slidetimer.setCountdown(seconds: number): voidtimer.remaining(): number | null // null if no countdown settimer.pause(): voidtimer.resume(): voidtimer.reset(): voidExample — display a live clock and countdown:
setInterval(() => { const elapsed = presentation.presenter.timer.elapsed(); const remaining = presentation.presenter.timer.remaining();
elapsedEl.textContent = formatSeconds(elapsed); if (remaining !== null) countdownEl.textContent = formatSeconds(remaining);}, 1000);Timer color conventions:
| Remaining | Indicator |
|---|---|
| > 5 min | Green |
| 1–5 min | Yellow |
| < 1 min | Red (flashing) |
Laser pointer
Section titled “Laser pointer”The laser position is broadcast to all audience clients as normalized coordinates (0–1).
// Enable laserpresentation.presenter.laser.enable();
// Track mouse on the speaker's current-slide elementcurrentSlideEl.onmousemove = (e) => { if (!presentation.presenter.laser.isEnabled()) return; const rect = currentSlideEl.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; presentation.presenter.laser.move(x, y);};
// Disablepresentation.presenter.laser.disable();Rendering on the audience side:
presentation.presenter.subscribe(state => { if (state.laser.enabled && state.laser.position) { const { x, y } = state.laser.position; laserEl.style.left = `${x * 100}%`; laserEl.style.top = `${y * 100}%`; laserEl.style.display = 'block'; } else { laserEl.style.display = 'none'; }});Blackout
Section titled “Blackout”Temporarily hide the audience’s view (useful for breaks or Q&A):
presentation.presenter.blackout(true); // audience sees black screenpresentation.presenter.blackout(false); // resumeAudience sync
Section titled “Audience sync”Audience clients subscribe to presenter state and follow the current slide. They do not need to call start():
presentation.presenter.subscribe(state => { if (!presentation.presenter.isPresenter()) { goToSlide(state.currentSlideIndex); renderLaser(state.laser); if (state.blackout) showBlackout(); }});To check whether anyone is currently presenting:
presentation.presenter.isActive(); // true if presentation is runningpresentation.presenter.isPresenter(); // true if this client is the presenterOptional audience self-navigation
Section titled “Optional audience self-navigation”If no one is presenting, or if the presenter enables it, viewers can navigate independently:
// Set on the presentation to allow audience navigationpresentation.meta.set({ allowAudienceNavigation: true });Stopping a presentation
Section titled “Stopping a presentation”presentation.presenter.stop();// Clears presentationState.isPresenting// Audience returns to normal view// Per-slide timing data is saved to the timings mapTransferring presenter control
Section titled “Transferring presenter control”Only one user can present at a time. To hand off:
// Current presenterpresentation.presenter.stop();
// New presenter (separate client)await presentation.presenter.start();State reference
Section titled “State reference”Full state object returned by presenter.state() and presenter.subscribe():
interface PresentationState { isPresenting: boolean; presenterId: string | null; presenterName: string | null; currentSlideIndex: number; totalSlides: number; startedAt: number | null; // unix timestamp ms slideStartedAt: number | null; countdown: { total: number; remaining: number; paused: boolean; } | null; laser: { enabled: boolean; position: { x: number; y: number } | null; }; blackout: boolean;}Version history
Section titled “Version history”Create a named version before a presentation to make rollback safe:
await presentation.versions.create('Before board meeting', 'Pre-presentation backup');After presenting, compare what changed:
const diff = await presentation.versions.diff('ver_before', 'ver_after');console.log(`${diff.slides.modified.length} slides changed`);Full versions API:
versions.list(): Promise<Version[]>versions.create(name: string, description?: string): Promise<Version>versions.restore(versionId: string): Promise<void> // auto-saves current state firstversions.diff(fromId: string, toId: string): Promise<VersionDiff>versions.delete(versionId: string): Promise<boolean>versions.subscribe(handler: (versions: Version[]) => void): () => voidAuto-saves trigger: every 5 minutes if changes exist, before a presentation starts, and when the last editor disconnects. The 10 most recent auto-saves are retained; named versions are never auto-deleted.
Permissions
Section titled “Permissions”| Role | Can present | Can view published | Can edit |
|---|---|---|---|
owner | Yes | Yes | Yes |
editor | Yes | Yes | Yes |
viewer | No | Yes | No |
| Anonymous | No | Based on visibility | No |
Manage collaborators via sdk.collaborators:
await sdk.collaborators.add(['colleague@example.com'], 'editor');await sdk.collaborators.add(['stakeholder@example.com'], 'viewer');Publishing the shareable link
Section titled “Publishing the shareable link”The published URL (shareout.site/p/{slug}) is always available to the audience. Control visibility and embedding:
presentation.publish.setVisibility('public'); // 'private' | 'workspace' | 'public'const url = presentation.publish.getUrl();Embed in an external page:
<iframe src="https://shareout.site/embed/my-deck/" width="100%" height="600" frameborder="0"></iframe>Embedding is available for public presentations. Restrict to specific origins via PATCH /v1/artifacts/{id} with embed_origins.
(unlisted is a retired legacy alias, still accepted on the API and treated as public.)