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:
'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:
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:
'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:
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:
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>.
Here’s a form demo showing how inputs work without triggering slide navigation:
'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 >
);
}
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:
< 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
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
Use 'use client' directive
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).
Test keyboard interactions
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