Skip to main content
View speaker notes on your phone while presenting. The notes page automatically follows along as you navigate slides with keyboard controls.

How It Works

Phone sync uses a simple polling mechanism:
  1. Presenter deck (laptop): When you navigate with arrow keys or spacebar, SlideDeck POSTs the current slide number to the sync endpoint
  2. Sync endpoint: Stores the current slide state in server memory
  3. Notes viewer (phone): Polls the endpoint every 500ms and displays the corresponding note
The notes array index matches the slide order: notes[slide - 1] displays the note for the current slide.
Sync state lives in server memory. This works perfectly for next dev or single-server deployments, but won’t persist across serverless function invocations on platforms like Vercel.

Setup

1

Create the sync API route

Export the pre-built handlers from nextjs-slides/sync:
app/api/nxs-sync/route.ts
export { GET, POST } from 'nextjs-slides/sync';
This creates an in-memory sync endpoint that stores { slide, total } state.
2

Add syncEndpoint to SlideDeck

Pass the sync endpoint to your deck layout:
app/slides/layout.tsx
import fs from 'fs';
import path from 'path';
import { SlideDeck, parseSpeakerNotes } from 'nextjs-slides';
import { slides } from './slides';

const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8')
);

export default function SlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SlideDeck
      slides={slides}
      speakerNotes={notes}
      syncEndpoint="/api/nxs-sync"
    >
      {children}
    </SlideDeck>
  );
}
3

Create the notes page

Use the same notes.md file and parseSpeakerNotes call to ensure indices match:
app/notes/page.tsx
import fs from 'fs';
import path from 'path';
import { parseSpeakerNotes, SlideNotesView } from 'nextjs-slides';

const notes = parseSpeakerNotes(
  fs.readFileSync(path.join(process.cwd(), 'app/slides/notes.md'), 'utf-8')
);

export default function NotesPage() {
  return <SlideNotesView notes={notes} syncEndpoint="/api/nxs-sync" />;
}
4

Open on your phone

On the same network, navigate to:
http://<your-ip>:3000/notes
The notes will auto-sync as you present.

SlideNotesView Component

SlideNotesView is a phone-optimized client component that displays speaker notes:
<SlideNotesView
  notes={notes}
  syncEndpoint="/api/nxs-sync"
  pollInterval={500}
/>

Props

PropTypeDefaultDescription
notes(string | null)[]requiredSpeaker notes array. Indices 0…slides-1 match slides; extras are demo notes.
syncEndpointstringrequiredAPI endpoint created with the sync route handlers.
pollIntervalnumber500Polling interval in milliseconds.

UI Features

  • Header: Shows “Slide X / Y” during presentation, “Demo X / Y” for demo notes
  • Connection indicator: Green dot when connected, red when disconnected
  • Manual controls: Prev/Next buttons for manual override or demo notes
  • Dark theme: Optimized for low-light presentation environments
  • Responsive text: Scales from 18px to 28px based on viewport

Sync Endpoint Implementation

The sync endpoint is a simple in-memory store:
src/sync.ts
let currentSlide = 1;
let totalSlides = 1;

export async function GET() {
  return Response.json({ slide: currentSlide, total: totalSlides });
}

export async function POST(request: Request) {
  const body = await request.json();
  if (typeof body.slide === 'number') currentSlide = body.slide;
  if (typeof body.total === 'number') totalSlides = body.total;
  return Response.json({ slide: currentSlide, total: totalSlides });
}
The SlideDeck component calls POST when navigating, and SlideNotesView polls GET to stay in sync.

Demo Notes Behavior

When you have extra sections in your notes file beyond the slide count:
  1. During slides: Phone auto-syncs to the current slide
  2. After last slide: Header changes to “Demo 1 / 2”
  3. Manual control: Use Prev/Next buttons to step through demo notes
  4. No auto-sync: Phone stays on the current demo note until you manually advance
This lets you control the pacing of live demos without the notes jumping around.

Troubleshooting

Notes are out of sync
  • Ensure syncEndpoint is set on both SlideDeck and SlideNotesView
  • Verify both use the same notes.md file and parsing options
  • Check that your API route is accessible at the specified path
Connection indicator is red
  • Verify the sync endpoint is running and accessible
  • Check browser console for CORS or network errors
  • Ensure phone and laptop are on the same network
Notes show document title instead of slide 1
  • Use parseSpeakerNotes(markdown, { stripLeadingTitle: true })
Notes work in dev but not production (Vercel)
  • The in-memory store can hit different serverless instances
  • For production, implement a shared store (Redis, KV, etc.)

Build docs developers (and LLMs) love