Skip to main content
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

KeyAction
(Right Arrow)Next slide
SpaceNext slide
(Left Arrow)Previous slide

How It Works

SlideDeck registers a global keydown listener that responds to navigation keys:
slide-deck.tsx
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:
  1. Form controlsinput, textarea, select, [contenteditable="true"]
  2. Interactive demos — Elements with [data-slide-interactive]
This prevents accidental slide changes when typing or interacting with components.

Form Controls

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:
slide-deck.tsx
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>
When a navigation key is pressed:
  1. Check route — Is the current pathname a slide route?
  2. Check focus — Is the target inside an interactive area?
  3. Prevent default — Stop browser scroll/navigation
  4. Calculate target — Clamp to valid slide range
  5. Sync — POST to syncEndpoint (if configured)
  6. Transition — Start ViewTransition with directional type
  7. Navigaterouter.push() to new slide URL
slide-deck.tsx
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:
slide-deck.tsx
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:
slide-deck.tsx
<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 slidegoTo(-1) clamps to 0
  • After last slidegoTo(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:
slide-deck.tsx
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.

Build docs developers (and LLMs) love