Skip to main content
The SlideDemo component lets you embed live, interactive React components in your slides. Keyboard navigation (arrow keys and space) is automatically disabled inside the demo so users can interact without accidentally advancing slides.

Basic Interactive Demo

Here’s a simple counter demo:
Counter.tsx
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex items-center justify-center gap-6">
      <button
        onClick={() => setCount((c) => c - 1)}
        className="bg-muted hover:bg-muted/80 flex h-10 w-10 items-center justify-center rounded-lg text-lg font-medium transition-colors"
      >

      </button>
      <span className="text-foreground w-12 text-center font-mono text-3xl font-bold tabular-nums">
        {count}
      </span>
      <button
        onClick={() => setCount((c) => c + 1)}
        className="bg-muted hover:bg-muted/80 flex h-10 w-10 items-center justify-center rounded-lg text-lg font-medium transition-colors"
      >
        +
      </button>
    </div>
  );
}
Embed it in a slide using SlideDemo:
slides.tsx
import { Slide, SlideBadge, SlideTitle, SlideSubtitle, SlideDemo, SlideNote } from 'nextjs-slides';
import { Counter } from '@/app/slides/_components/Counter';

<Slide key="demo">
  <SlideBadge>SlideDemo</SlideBadge>
  <SlideTitle className="text-3xl sm:text-4xl md:text-5xl">
    Interactive components
  </SlideTitle>
  <SlideSubtitle>
    Embed live React components. Arrow keys and space are disabled inside so
    inputs work without advancing slides
  </SlideSubtitle>
  <SlideDemo label="Live counter">
    <Counter />
  </SlideDemo>
  <SlideNote>
    Props: label (uppercase header), className · Container tracks max height
    to prevent layout jumps on re-render
  </SlideNote>
</Slide>
The label prop adds an uppercase header above the demo. The container automatically tracks max height to prevent layout jumps when content re-renders.

Demo with Theme Toggle

Here’s a more complex example with a theme toggle:
ThemeToggle.tsx
'use client';

import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

// Icon components (Sun, Moon, Monitor)
// ... see full source in demo app ...

export function ThemeToggle() {
  const { theme, setTheme, resolvedTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return (
      <div className="border-foreground/20 bg-foreground/5 flex h-14 w-32 items-center justify-center rounded-xl border opacity-50">
        <span className="text-muted-foreground text-sm">Loading...</span>
      </div>
    );
  }

  return (
    <div className="flex flex-col items-center gap-4">
      <div className="border-foreground/20 bg-foreground/5 inline-flex rounded-xl border p-1">
        <button
          type="button"
          onClick={() => setTheme('light')}
          className={`rounded-lg p-2 transition-colors ${
            theme === 'light'
              ? 'bg-foreground/10 text-foreground'
              : 'text-muted-foreground hover:text-foreground'
          }`}
        >
          <SunIcon />
        </button>
        <button
          type="button"
          onClick={() => setTheme('dark')}
          className={`rounded-lg p-2 transition-colors ${
            theme === 'dark'
              ? 'bg-foreground/10 text-foreground'
              : 'text-muted-foreground hover:text-foreground'
          }`}
        >
          <MoonIcon />
        </button>
        <button
          type="button"
          onClick={() => setTheme('system')}
          className={`rounded-lg p-2 transition-colors ${
            theme === 'system'
              ? 'bg-foreground/10 text-foreground'
              : 'text-muted-foreground hover:text-foreground'
          }`}
        >
          <MonitorIcon />
        </button>
      </div>
      <p className="text-muted-foreground text-center text-sm">
        {resolvedTheme === 'dark' ? 'Dark' : 'Light'} mode
        {theme === 'system' && ' (system)'}
      </p>
    </div>
  );
}
Use it in a slide:
slides.tsx
import { ThemeToggle } from '@/app/slides/_components/ThemeToggle';

<Slide key="theme">
  <SlideBadge>Theme inheritance</SlideBadge>
  <SlideTitle className="text-3xl sm:text-4xl md:text-5xl">
    Slides inherit your app theme
  </SlideTitle>
  <SlideSubtitle>
    The deck, code blocks, and all primitives use your app CSS variables:
    --foreground, --background, --muted-foreground, --nxs-code-*, --sh-*, etc.
    No scoping. Slides follow whatever theme your layout defines.
  </SlideSubtitle>
  <SlideDemo label="Toggle theme">
    <ThemeToggle />
  </SlideDemo>
</Slide>

Demo + Source Code Pattern

A common pattern is showing a live demo alongside its source code using SlideSplitLayout:
slides.tsx
import {
  SlideSplitLayout,
  SlideBadge,
  SlideTitle,
  SlideSubtitle,
  SlideDemo,
  SlideCode,
} from 'nextjs-slides';
import { Counter } from '@/app/slides/_components/Counter';

<SlideSplitLayout
  key="demo-code"
  left={
    <>
      <SlideBadge className="font-pixel">Pattern</SlideBadge>
      <SlideTitle className="text-3xl sm:text-4xl md:text-5xl">
        Demo + source
      </SlideTitle>
      <SlideSubtitle>
        Pair a live component with its source code using SlideSplitLayout
      </SlideSubtitle>
      <SlideDemo label="Try it">
        <Counter />
      </SlideDemo>
    </>
  }
  right={
    <SlideCode title="Counter.tsx">{`'use client';
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}`}</SlideCode>
  }
/>
SlideSplitLayout is a full-viewport component and should not be nested inside <Slide>. It’s a top-level alternative to <Slide>.

Interactive Form Example

Here’s a form demo showing how inputs work without triggering slide navigation:
ContactForm.tsx
'use client';

import { useState } from 'react';

export function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true);
    setTimeout(() => setSubmitted(false), 2000);
  };

  return (
    <form onSubmit={handleSubmit} className="flex w-full max-w-md flex-col gap-4">
      <input
        type="text"
        placeholder="Name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        className="bg-background border-foreground/20 rounded-lg border px-4 py-2"
      />
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="bg-background border-foreground/20 rounded-lg border px-4 py-2"
      />
      <button
        type="submit"
        className="bg-primary text-primary-foreground rounded-lg px-4 py-2 font-medium"
      >
        {submitted ? '✓ Submitted!' : 'Submit'}
      </button>
    </form>
  );
}
slides.tsx
import { ContactForm } from '@/app/slides/_components/ContactForm';

<Slide key="form-demo">
  <SlideBadge>Forms</SlideBadge>
  <SlideTitle>Interactive Forms</SlideTitle>
  <SlideSubtitle>
    Type in inputs without triggering slide navigation
  </SlideSubtitle>
  <SlideDemo label="Try it">
    <ContactForm />
  </SlideDemo>
</Slide>

Multiple Demos on One Slide

You can include multiple demo components:
slides.tsx
<Slide key="multiple-demos" align="left">
  <SlideBadge>Demos</SlideBadge>
  <SlideTitle>Multiple Components</SlideTitle>
  
  <div className="flex gap-8">
    <SlideDemo label="Counter">
      <Counter />
    </SlideDemo>
    
    <SlideDemo label="Toggle">
      <ThemeToggle />
    </SlideDemo>
  </div>
</Slide>

Keyboard Navigation Behavior

Inside SlideDemo, these keys are blocked from advancing slides:
  • Arrow keys (←, →, ↑, ↓)
  • Space bar
  • Enter key
This allows your demo components to use these keys for their own interactions (e.g., moving focus, submitting forms, toggling states) without accidentally navigating to the next slide.

Complete Interactive Presentation Example

app/slides/slides.tsx
import {
  Slide,
  SlideBadge,
  SlideCode,
  SlideDemo,
  SlideHeaderBadge,
  SlideSplitLayout,
  SlideSubtitle,
  SlideTitle,
} from 'nextjs-slides';
import { Counter } from '@/app/slides/_components/Counter';
import { ThemeToggle } from '@/app/slides/_components/ThemeToggle';

export const slides: React.ReactNode[] = [
  // Title
  <Slide key="title" align="left">
    <SlideHeaderBadge>Interactive Workshop</SlideHeaderBadge>
    <SlideTitle>Live React Components</SlideTitle>
    <SlideSubtitle>Building interactive presentations</SlideSubtitle>
  </Slide>,

  // Demo only
  <Slide key="counter-demo">
    <SlideBadge>Demo</SlideBadge>
    <SlideTitle>Live Counter</SlideTitle>
    <SlideSubtitle>Click the buttons - arrow keys won't advance the slide</SlideSubtitle>
    <SlideDemo label="Interactive">
      <Counter />
    </SlideDemo>
  </Slide>,

  // Demo + code split
  <SlideSplitLayout
    key="demo-code"
    left={
      <>
        <SlideBadge>Pattern</SlideBadge>
        <SlideTitle>Demo + Source</SlideTitle>
        <SlideSubtitle>Show the component and its implementation</SlideSubtitle>
        <SlideDemo label="Try it">
          <Counter />
        </SlideDemo>
      </>
    }
    right={
      <SlideCode title="Counter.tsx">{`'use client';
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}`}</SlideCode>
    }
  />,

  // Theme demo
  <Slide key="theme">
    <SlideBadge>Theming</SlideBadge>
    <SlideTitle>Theme Inheritance</SlideTitle>
    <SlideSubtitle>Demos inherit your app's theme system</SlideSubtitle>
    <SlideDemo label="Toggle theme">
      <ThemeToggle />
    </SlideDemo>
  </Slide>,
];

Best Practices

All interactive demo components must be client components. Add 'use client'; at the top of your component file.
Each demo should illustrate one concept. Don’t try to pack too much functionality into a single demo component.
Make sure your demos give clear visual feedback when interacted with (button states, transitions, confirmation messages).
Verify that your demo’s keyboard interactions work correctly and don’t conflict with slide navigation.

Next Steps

Code Slides

Learn how to show code alongside demos

Custom Styling

Style your demo components with custom themes

Content Components

Explore SlideDemo props and options

Layout Components

Use SlideSplitLayout for demo + code patterns

Build docs developers (and LLMs) love