Presenter Modes
Enable real-time viewport synchronization for presentations and classroom scenarios.
Overview
Presenter Modes provide powerful tools for teachers to control and monitor student viewports during collaborative sessions. Two complementary modes enable different teaching scenarios:
| Mode | Direction | Use Case |
|---|---|---|
| Follow Me | Teacher → Students | Presentations, demonstrations, guided lessons |
| Spectate | Student → Teacher | Student monitoring, individual help, assessment |
Both modes integrate seamlessly with Frames for structured presentations and work especially well in educational settings.
Key Benefits
- Real-time synchronization at 20 updates/sec (Follow Me) or 10 updates/sec (Spectate)
- Flexible control with breakaway mechanism (students can opt-out)
- Privacy options including stealth spectating
- Integration with Frames for presentation-style navigation
- Minimal latency using WebSocket transport
Follow Me Mode
How It Works
When a teacher enables Follow Me mode, their viewport (scroll position and zoom level) is broadcast to all participants. Students' viewports automatically synchronize to match the teacher's view in real-time.
┌─────────────────────────────────────────────────────┐
│ FOLLOW ME MODE FLOW │
├─────────────────────────────────────────────────────┤
│ │
│ Teacher moves viewport │
│ │ │
│ ├─► WebSocket broadcast │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Student 1│ │Student 2│ │Student 3│ │
│ │ syncs │ │ syncs │ │ syncs │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────┘Use Cases
- Classroom Presentations: Teacher explains content while students follow along
- Step-by-step Tutorials: Guide students through complex diagrams or workflows
- Virtual Lectures: Ensure all remote students see the same content simultaneously
- Group Demonstrations: Show solutions to problems with automatic viewport control
Starting Follow Me
Teachers can enable Follow Me mode through the Teacher Panel UI or programmatically via WebSocket:
// Connect to board WebSocket
const socket = io('https://api.boardapi.io/whiteboard', {
query: { token: 'your-jwt-token' }
});
// Join the board
socket.emit('board:join', { boardUuid: 'your-board-uuid' });
// Start Follow Me mode
socket.emit('followme:start', {
boardId: 'your-board-uuid',
allowBreakaway: true, // Let students opt-out
smoothAnimation: true // Smooth viewport transitions
});Configuration Options
interface FollowMeConfig {
enabled: boolean; // Mode is active
allowBreakaway: boolean; // Students can disconnect
showFollowerCount: boolean; // Display "12/15 following"
syncInterval: number; // Update frequency in ms (default: 50)
smoothAnimation: boolean; // Smooth scroll transitions
}Broadcasting Viewport Updates
As the teacher moves their viewport, the client should send throttled updates:
// Throttle to ~20 updates/sec (every 50ms)
let lastUpdate = 0;
const THROTTLE_MS = 50;
function onViewportChange(scrollX, scrollY, zoom) {
const now = Date.now();
if (now - lastUpdate < THROTTLE_MS) return;
lastUpdate = now;
socket.emit('followme:viewport', {
boardId: 'your-board-uuid',
scrollX: scrollX,
scrollY: scrollY,
zoom: zoom
});
}Receiving Viewport Updates (Students)
Students listen for viewport updates and synchronize their view:
// Listen for Follow Me mode activation
socket.on('followme:mode', (data) => {
if (data.enabled) {
console.log(`Following ${data.leaderName}`);
// Show "Following..." banner
showFollowBanner(data.leaderName);
} else {
console.log('Follow Me mode stopped');
hideFollowBanner();
}
});
// Listen for viewport updates
socket.on('followme:viewport', (data) => {
// Sync viewport to match teacher
syncViewport({
scrollX: data.scrollX,
scrollY: data.scrollY,
zoom: data.zoom
});
});Breakaway Mechanism
If allowBreakaway is enabled, students can temporarily disconnect from Follow Me mode:
// Student breaks away (manual scroll/zoom)
socket.emit('followme:breakaway', {
boardId: 'your-board-uuid'
});
// Student rejoins
socket.emit('followme:rejoin', {
boardId: 'your-board-uuid'
});The teacher receives status updates showing who is following:
socket.on('followme:status', (data) => {
console.log('Following:', data.following); // ['user-1', 'user-2']
console.log('Breakaway:', data.breakaway); // ['user-3']
// Update UI to show "12/15 following"
});Stopping Follow Me
socket.emit('followme:stop', {
boardId: 'your-board-uuid'
});
// All students receive:
// followme:mode { enabled: false, ... }Spectate Mode
How It Works
Spectate mode inverts the viewport control: the teacher selects a specific student and receives their viewport updates. This enables the teacher to see exactly what the student is viewing and working on.
┌─────────────────────────────────────────────────────┐
│ SPECTATE MODE FLOW │
├─────────────────────────────────────────────────────┤
│ │
│ Student works independently │
│ │ │
│ ├─► Viewport updates │
│ │ │
│ ▼ │
│ Teacher's view syncs to student │
│ (Teacher sees student's viewport) │
│ │
└─────────────────────────────────────────────────────┘Use Cases
- Individual Help: Teacher monitors struggling student's work
- Assessment Monitoring: Discreetly observe student during exams
- Progress Checking: Quick view of what each student is doing
- Remote Tutoring: Follow student's thought process in real-time
Starting Spectate
Teacher initiates spectating by selecting a target student:
// Start spectating a student
socket.emit('spectate:start', {
boardId: 'your-board-uuid',
targetParticipantId: 'student-user-id',
stealthMode: false // Student sees "Teacher is watching" indicator
});Stealth Mode
When stealthMode: true, the student does NOT receive a notification that they're being observed:
// Stealth spectating (student unaware)
socket.emit('spectate:start', {
boardId: 'your-board-uuid',
targetParticipantId: 'student-user-id',
stealthMode: true
});Use stealth mode responsibly: Inform students about monitoring policies in advance for ethical transparency.
Student Sends Viewport Updates
The spectated student's client automatically sends viewport updates (throttled to ~10/sec):
// Student sends viewport updates when being spectated
let lastSpectateUpdate = 0;
const SPECTATE_THROTTLE_MS = 100; // 10 updates/sec
function onStudentViewportChange(scrollX, scrollY, zoom, cursorX, cursorY) {
const now = Date.now();
if (now - lastSpectateUpdate < SPECTATE_THROTTLE_MS) return;
lastSpectateUpdate = now;
socket.emit('spectate:viewport', {
boardId: 'your-board-uuid',
scrollX: scrollX,
scrollY: scrollY,
zoom: zoom,
cursorX: cursorX, // Optional: student cursor position
cursorY: cursorY
});
}Teacher Receives Viewport Updates
Teacher's client listens for student viewport updates:
// Listen for spectate viewport updates
socket.on('spectate:viewport', (data) => {
console.log(`Spectating student ${data.targetId}`);
// Sync teacher's viewport to student's view
syncViewport({
scrollX: data.scrollX,
scrollY: data.scrollY,
zoom: data.zoom
});
// Optionally show student's cursor position
if (data.cursorX && data.cursorY) {
showRemoteCursor(data.cursorX, data.cursorY);
}
});Stopping Spectate
socket.emit('spectate:stop', {
boardId: 'your-board-uuid'
});
// Student receives (if not in stealth mode):
// spectate:end { observerId: 'teacher-id', ... }Configuration Options
interface SpectateConfig {
targetParticipantId: string; // Student to observe
stealthMode: boolean; // Hidden observation
showCursor: boolean; // Show teacher cursor to student
canInteract: boolean; // Teacher can draw (co-edit mode)
}Note: showCursor and canInteract are advanced options. Currently, spectate mode is viewport-only by default.
Integration with Frames
Presenter Modes work seamlessly with Frames for structured presentations.
Follow Me + Frames
When Follow Me mode is active alongside Frame navigation:
// Teacher navigates to a frame
socket.emit('frame:navigate', {
boardId: 'your-board-uuid',
frameId: 'frame-uuid',
animate: true
});
// All following students automatically:
// 1. Receive frame:navigated event
// 2. Receive followme:viewport event syncing to frame boundsCombined Flow:
- Teacher clicks "Next Frame" in presentation mode
- Frame navigation event → students' viewports scroll to frame
- Follow Me ensures students stay synced even within the frame
Spectate + Frames
Spectate mode shows which frame the student is currently viewing:
socket.on('spectate:viewport', (data) => {
// Check if student is within a known frame
const currentFrame = getFrameAtPosition(data.scrollX, data.scrollY);
if (currentFrame) {
console.log(`Student is in frame: ${currentFrame.name}`);
// Show in UI: "Spectating Ivan - Frame: Practice Exercise"
} else {
console.log('Student is outside frames');
}
});Lock Modes + Presenter Modes
| Frame Lock Mode | Follow Me | Spectate |
|---|---|---|
none | Works freely | Works freely |
soft | Works, overrides soft warning | Works freely |
hard | Works, overrides lock (teacher control) | Student cannot leave, spectate shows locked frame |
Hard lock behavior: When a frame is locked with hard mode, students cannot leave even if they break away from Follow Me. The teacher can still navigate them via Follow Me, which takes precedence.
Complete Example: Virtual Classroom
This example demonstrates a complete virtual classroom workflow using Follow Me and Spectate:
// Teacher Dashboard Component
class TeacherDashboard {
constructor(socket, boardId) {
this.socket = socket;
this.boardId = boardId;
this.followMeActive = false;
this.spectatingStudent = null;
}
// Start presentation mode
startPresentation() {
this.socket.emit('followme:start', {
boardId: this.boardId,
allowBreakaway: true,
smoothAnimation: true
});
this.followMeActive = true;
// Listen for follower status
this.socket.on('followme:status', (data) => {
this.updateFollowerCount(data.following.length, data.breakaway.length);
});
}
// Navigate to next slide
nextSlide(frameId) {
this.socket.emit('frame:navigate', {
boardId: this.boardId,
frameId: frameId,
animate: true
});
// Follow Me automatically syncs students to this frame
}
// Monitor specific student
spectateStudent(studentId) {
// Stop spectating previous student if any
if (this.spectatingStudent) {
this.socket.emit('spectate:stop', { boardId: this.boardId });
}
// Start spectating new student
this.socket.emit('spectate:start', {
boardId: this.boardId,
targetParticipantId: studentId,
stealthMode: false
});
this.spectatingStudent = studentId;
// Receive student's viewport
this.socket.on('spectate:viewport', (data) => {
this.syncToStudentView(data);
});
}
// Stop spectating and return to presentation
stopSpectate() {
this.socket.emit('spectate:stop', { boardId: this.boardId });
this.spectatingStudent = null;
}
// End presentation
endPresentation() {
this.socket.emit('followme:stop', { boardId: this.boardId });
this.followMeActive = false;
}
}
// Student Client Component
class StudentBoard {
constructor(socket, boardId) {
this.socket = socket;
this.boardId = boardId;
this.isFollowing = false;
this.isBeingSpectated = false;
this.setupListeners();
}
setupListeners() {
// Handle Follow Me mode
this.socket.on('followme:mode', (data) => {
if (data.enabled) {
this.isFollowing = true;
this.showBanner(`Following ${data.leaderName}`);
} else {
this.isFollowing = false;
this.hideBanner();
}
});
// Sync viewport in Follow Me
this.socket.on('followme:viewport', (data) => {
if (this.isFollowing) {
this.syncViewport(data.scrollX, data.scrollY, data.zoom);
}
});
// Handle being spectated
this.socket.on('spectate:start', (data) => {
if (!data.stealthMode) {
this.isBeingSpectated = true;
this.showSpectateIndicator(data.observerName);
}
});
this.socket.on('spectate:end', () => {
this.isBeingSpectated = false;
this.hideSpectateIndicator();
});
}
// Student manually breaks away
breakAway() {
this.socket.emit('followme:breakaway', { boardId: this.boardId });
this.isFollowing = false;
this.showRejoinButton();
}
// Student rejoins Follow Me
rejoin() {
this.socket.emit('followme:rejoin', { boardId: this.boardId });
this.isFollowing = true;
this.hideRejoinButton();
}
// Send viewport updates when being spectated
onViewportChange(scrollX, scrollY, zoom, cursorX, cursorY) {
if (this.isBeingSpectated) {
this.socket.emit('spectate:viewport', {
boardId: this.boardId,
scrollX, scrollY, zoom, cursorX, cursorY
});
}
}
}WebSocket Events Reference
Follow Me Events
Client → Server:
| Event | Payload | Description |
|---|---|---|
followme:start | { boardId, allowBreakaway, smoothAnimation } | Teacher starts Follow Me |
followme:stop | { boardId } | Teacher stops Follow Me |
followme:viewport | { boardId, scrollX, scrollY, zoom } | Teacher sends viewport update |
followme:breakaway | { boardId } | Student disconnects from Follow Me |
followme:rejoin | { boardId } | Student reconnects to Follow Me |
Server → Client:
| Event | Payload | Description |
|---|---|---|
followme:mode | { enabled, leaderId, leaderName, config } | Follow Me mode changed |
followme:viewport | { scrollX, scrollY, zoom, timestamp } | Viewport update from teacher |
followme:status | { following[], breakaway[] } | Follower status update |
Spectate Events
Client → Server:
| Event | Payload | Description |
|---|---|---|
spectate:start | { boardId, targetParticipantId, stealthMode } | Teacher starts spectating |
spectate:stop | { boardId } | Teacher stops spectating |
spectate:viewport | { boardId, scrollX, scrollY, zoom, cursorX, cursorY } | Student sends viewport update |
Server → Client:
| Event | Payload | Description |
|---|---|---|
spectate:start | { observerId, observerName, targetId, stealthMode } | Spectating started (sent to student if not stealth) |
spectate:viewport | { targetId, scrollX, scrollY, zoom, cursorX, cursorY } | Student viewport update (sent to teacher) |
spectate:end | { observerId, targetId } | Spectating stopped |
For complete WebSocket API documentation, see WebSocket API Reference.
Best Practices
Performance
- Throttle viewport updates: Don't send updates more frequently than 20/sec (Follow Me) or 10/sec (Spectate)
- Use smooth animations: Enable
smoothAnimation: truefor better UX - Monitor follower count: Show teachers when students break away
- Cleanup on disconnect: Always stop Follow Me/Spectate when teacher leaves
User Experience
- Visual indicators: Show clear "Following Teacher" or "Teacher Watching" banners
- Breakaway button: Make it easy for students to opt-out when needed
- Rejoin option: Provide a "Return to Presentation" button for breakaway students
- Frame integration: Combine with Frames for structured presentations
Privacy & Ethics
- Inform students: Disclose monitoring capabilities in your privacy policy
- Stealth mode sparingly: Use stealth spectating only when pedagogically justified
- Clear indicators: When not in stealth, show visible "Being Watched" notifications
- Parent consent: For minors, ensure parental consent for monitoring features
Accessibility
- Keyboard controls: Support Escape key to exit spectate/breakaway
- Screen readers: Announce Follow Me/Spectate mode changes
- Focus management: Don't override keyboard navigation during Follow Me
- Contrast: Ensure indicators have sufficient contrast
Common Issues
Students not syncing in Follow Me
Check:
- Verify
followme:modeevent received withenabled: true - Ensure viewport sync function is called on
followme:viewportevents - Check throttling - updates should arrive ~20/sec
- Verify students haven't manually broken away
Spectate viewport not updating
Check:
- Student is sending
spectate:viewportevents (check network tab) - Throttling is correct (~10/sec, not too aggressive)
- Teacher is listening to
spectate:viewportevents - Target student is still connected to the board
Lag or jitter in viewport sync
Solutions:
- Implement smooth interpolation between viewport positions
- Increase throttle interval if network is slow (100ms instead of 50ms)
- Use
smoothAnimation: truefor Follow Me mode - Consider reducing zoom precision to 2 decimal places
Students getting "stuck" in Follow Me
Solution: Implement timeout to auto-break away if teacher disconnects:
let followMeTimeout;
socket.on('followme:mode', (data) => {
if (data.enabled) {
// Reset timeout on each viewport update
clearTimeout(followMeTimeout);
followMeTimeout = setTimeout(() => {
// Teacher hasn't sent updates for 5 seconds
autoBreakAway();
}, 5000);
}
});Quota & Limits
Presenter Modes are available on specific tiers. Check your organization's plan:
| Feature | Starter | Professional | Business | Enterprise |
|---|---|---|---|---|
| Follow Me Mode | ❌ | ✅ | ✅ | ✅ |
| Spectate Mode | ❌ | ✅ | ✅ | ✅ |
| Stealth Spectate | ❌ | ❌ | ✅ | ✅ |
| Concurrent Spectate Sessions | - | 1 | 5 | Unlimited |
See Quota & Limits API for checking your current limits.
Next Steps
- WebSocket API - Complete WebSocket events reference
- Frames API - Combine with Frames for presentations
- Authentication - Secure WebSocket connections
- AI Agents - Automate presenter actions with AI
Support
Need help implementing Presenter Modes?
- Join our Developer Discord
- Email support: developers@boardapi.io
- View examples: GitHub Examples Repository