nextjs-slides provides built-in keyboard navigation for your slide deck. Arrow keys and spacebar advance slides, while automatically pausing in interactive areas like inputs and demos.
Key Bindings
| Key | Action |
|---|
→ (Right Arrow) | Next slide |
Space | Next slide |
← (Left Arrow) | Previous slide |
How It Works
SlideDeck registers a global keydown listener that responds to navigation keys:
useEffect(() => {
if (!isSlideRoute) return;
function onKeyDown(e: KeyboardEvent) {
const target = e.target as HTMLElement;
if (
target.closest('[data-slide-interactive]') ||
target.matches('input, textarea, select, [contenteditable="true"]')
) {
return;
}
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
goTo(current + 1);
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
goTo(current - 1);
}
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [current, goTo, isSlideRoute]);
Behavior
- Right Arrow / Space — Advances to the next slide (clamped to last slide)
- Left Arrow — Goes back to the previous slide (clamped to first slide)
- preventDefault — Prevents default browser behavior (e.g. page scroll on spacebar)
Keyboard navigation only activates on slide routes (e.g. /slides/1). Breakout pages like /slides/demo don’t respond to navigation keys.
Disabled in Interactive Areas
Keyboard navigation is automatically disabled when the focus is inside:
- Form controls —
input, textarea, select, [contenteditable="true"]
- Interactive demos — Elements with
[data-slide-interactive]
This prevents accidental slide changes when typing or interacting with components.
import { Slide, SlideTitle } from 'nextjs-slides';
<Slide key="input-demo">
<SlideTitle>Try It</SlideTitle>
<input
type="text"
placeholder="Type here — arrow keys won't advance slides"
className="border p-2"
/>
</Slide>
When the input is focused, arrow keys type text instead of navigating slides.
SlideDemo Component
Use <SlideDemo> to mark interactive areas:
import { Slide, SlideTitle, SlideDemo } from 'nextjs-slides';
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="flex gap-4">
<button onClick={() => setCount(count - 1)}>−</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
<Slide key="counter-demo">
<SlideTitle>Interactive Counter</SlideTitle>
<SlideDemo label="Try it">
<Counter />
</SlideDemo>
</Slide>
SlideDemo adds data-slide-interactive to its container, disabling keyboard navigation when focused inside.
How data-slide-interactive Works
The check uses closest('[data-slide-interactive]') to detect if the event target is inside a marked container:
const target = e.target as HTMLElement;
if (
target.closest('[data-slide-interactive]') ||
target.matches('input, textarea, select, [contenteditable="true"]')
) {
return;
}
This ensures nested interactive elements (buttons inside SlideDemo, inputs inside forms) all respect the interactive boundary.
Add data-slide-interactive to any custom component where you want to disable keyboard navigation:<div data-slide-interactive>
<YourCustomWidget />
</div>
Navigation Flow
When a navigation key is pressed:
- Check route — Is the current pathname a slide route?
- Check focus — Is the target inside an interactive area?
- Prevent default — Stop browser scroll/navigation
- Calculate target — Clamp to valid slide range
- Sync — POST to
syncEndpoint (if configured)
- Transition — Start ViewTransition with directional type
- Navigate —
router.push() to new slide URL
const goTo = useCallback(
(index: number) => {
const clamped = Math.max(0, Math.min(index, total - 1));
if (clamped === current) return;
const targetSlide = clamped + 1; // 1-based for sync API
syncSlide(targetSlide); // Immediate feedback for phone sync
startTransition(() => {
addTransitionType(
clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK
);
router.push(`${basePath}/${targetSlide}`);
});
},
[basePath, current, router, startTransition, syncSlide, total]
);
Directional Transitions
goTo uses addTransitionType() to tag the transition as forward or backward:
const TRANSITION_FORWARD = 'slide-forward';
const TRANSITION_BACK = 'slide-back';
addTransitionType(
clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK
);
This allows the ViewTransition to choose the correct animation direction:
<ViewTransition
key={pathname}
default="none"
enter={{
default: 'slide-from-right',
[TRANSITION_BACK]: 'slide-from-left',
[TRANSITION_FORWARD]: 'slide-from-right',
}}
exit={{
default: 'slide-to-left',
[TRANSITION_BACK]: 'slide-to-right',
[TRANSITION_FORWARD]: 'slide-to-left',
}}
>
<div>{children}</div>
</ViewTransition>
See Animations for details.
Clamping
Navigation is clamped to the slide range:
const clamped = Math.max(0, Math.min(index, total - 1));
if (clamped === current) return;
- Before first slide —
goTo(-1) clamps to 0
- After last slide —
goTo(total) clamps to total - 1
- No-op — If the clamped index equals the current slide, navigation is skipped
You can’t navigate past the first or last slide. The deck stays at the boundary and ignores additional key presses.
Only on Slide Routes
Keyboard navigation is only active when isSlideRoute is true:
useEffect(() => {
if (!isSlideRoute) return;
// ... register keydown listener
}, [current, goTo, isSlideRoute]);
Breakout pages (e.g. /slides/demo) don’t match the slide route pattern, so they don’t respond to arrow keys or spacebar.
Accessibility
- Spacebar — Provides an alternative to arrow keys (common in presentation tools)
- Visual feedback — Progress dots and counter show current position
- Prevent default — Stops spacebar from scrolling the page
For screen reader support, consider adding ARIA landmarks and live regions for slide transitions.