Skip to main content
SlideDeck is the top-level provider component that wraps your slides layout. It manages navigation state, keyboard events, ViewTransition animations, and optional presenter sync.

Role

SlideDeck provides the infrastructure for your presentation:
  • Keyboard navigation — Arrow keys and spacebar to navigate slides
  • ViewTransition animations — Smooth directional transitions powered by React 19
  • Progress indicators — Visual feedback with dots and a counter
  • Exit button — Optional × button when exitUrl is set
  • Presenter sync — Broadcasts slide changes to a notes page via API endpoint

Basic Setup

Place SlideDeck in your slides layout (e.g. app/slides/layout.tsx):
app/slides/layout.tsx
import { SlideDeck } from 'nextjs-slides';
import { slides } from './slides';

export default function SlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <SlideDeck slides={slides}>{children}</SlideDeck>;
}
SlideDeck is a client component, so your layout can stay a server component — no "use client" needed.

Configuration

Required Props

  • slides (ReactNode[]) — Your slides array
  • children (ReactNode) — Route content rendered by Next.js

Optional Props

  • basePath (string) — URL prefix for slide routes. Defaults to "/slides"
  • exitUrl (string) — URL for exit button (×). Shows in top-right when set
  • showProgress (boolean) — Show dot progress indicator. Defaults to true
  • showCounter (boolean) — Show “3 / 10” counter. Defaults to true
  • syncEndpoint (string) — API route for presenter ↔ phone sync
  • speakerNotes ((string | ReactNode | null)[]) — Notes per slide (same index)
  • className (string) — Additional class for the deck container

Common Configurations

With Speaker Notes and Sync

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>
  );
}

Multiple Decks

Use basePath for a different URL and className for scoped styling:
app/slides-alt/layout.tsx
import { SlideDeck } from 'nextjs-slides';
import { slides } from './slides';

export default function AltSlidesLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SlideDeck
      slides={slides}
      basePath="/slides-alt"
      exitUrl="/"
      className="slides-alt-deck"
    >
      {children}
    </SlideDeck>
  );
}
Use className to apply scoped font families or syntax highlighting themes for different decks.

Minimal (No UI)

Disable progress indicators for a clean presentation:
<SlideDeck
  slides={slides}
  showProgress={false}
  showCounter={false}
>
  {children}
</SlideDeck>

How It Works

Route Detection

SlideDeck uses the current pathname to determine if you’re on a slide route:
slide-deck.tsx
const slideRoutePattern = useMemo(
  () =>
    new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/(\\d+)$`),
  [basePath]
);
const isSlideRoute = slideRoutePattern.test(pathname);
If the pathname matches /slides/1, /slides/2, etc., it shows progress indicators and enables keyboard navigation. Pages like /slides/demo are breakout pages — they inherit the slide deck’s layout but don’t render navigation UI.

Prefetching

SlideDeck prefetches adjacent slides for instant navigation:
slide-deck.tsx
useEffect(() => {
  if (!isSlideRoute) return;
  if (current > 0) router.prefetch(`${basePath}/${current}`);
  if (current < total - 1) router.prefetch(`${basePath}/${current + 2}`);
}, [basePath, current, isSlideRoute, router, total]);

Presenter Sync

When syncEndpoint is provided, SlideDeck POSTs the current slide on navigation:
slide-deck.tsx
const syncSlide = useCallback(
  (slide: number) => {
    if (!syncEndpoint || !isSlideRoute) return;
    fetch(syncEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ slide, total }),
    }).catch(() => {});
  },
  [isSlideRoute, syncEndpoint, total]
);
The notes page polls this endpoint to stay in sync. See the Speaker Notes guide for setup.

Layout Requirements

SlideDeck must be the direct child of the layout. Wrapping it in a <div> can prevent the exit animation from running.

Correct

app/slides/layout.tsx
export default function SlidesLayout({ children }: { children: React.ReactNode }) {
  return (
    <SlideDeck slides={slides}>
      {children}
    </SlideDeck>
  );
}

Incorrect

app/slides/layout.tsx
export default function SlidesLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="wrapper">
      <SlideDeck slides={slides}>
        {children}
      </SlideDeck>
    </div>
  );
}
If you need scoped styles, use the className prop instead:
<SlideDeck slides={slides} className="font-pixel">
  {children}
</SlideDeck>

TypeScript

types.ts
export interface SlideDeckConfig {
  slides: React.ReactNode[];
  speakerNotes?: (string | React.ReactNode | null)[];
  basePath?: string;
  exitUrl?: string;
  showProgress?: boolean;
  showCounter?: boolean;
  syncEndpoint?: string;
  className?: string;
}

Build docs developers (and LLMs) love