Skip to main content
nextjs-slides uses React 19’s <ViewTransition> component to create smooth, directional animations between slides. Transitions are aware of navigation direction (forward/backward) and use CSS View Transition pseudo-elements for hardware-accelerated effects.

React 19 ViewTransitions

React 19 introduced the <ViewTransition> component and addTransitionType() API for declarative transitions. nextjs-slides wraps slide content with ViewTransition to enable coordinated enter/exit animations.

How It Works

SlideDeck wraps the current slide in a ViewTransition with directional animation types:
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>
  • key={pathname} — Forces a new ViewTransition when the route changes
  • default="none" — No animation by default
  • enter — Animation types for the incoming slide
  • exit — Animation types for the outgoing slide
ViewTransition captures the old and new DOM state, creating pseudo-elements (:view-transition-old and :view-transition-new) that can be animated with CSS.

Directional Transitions

When navigating, SlideDeck uses addTransitionType() to tag the transition:
slide-deck.tsx
const TRANSITION_FORWARD = 'slide-forward';
const TRANSITION_BACK = 'slide-back';

const goTo = useCallback(
  (index: number) => {
    const clamped = Math.max(0, Math.min(index, total - 1));
    if (clamped === current) return;
    const targetSlide = clamped + 1;
    syncSlide(targetSlide);
    startTransition(() => {
      addTransitionType(
        clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK
      );
      router.push(`${basePath}/${targetSlide}`);
    });
  },
  [basePath, current, router, startTransition, syncSlide, total]
);

Transition Types

  • TRANSITION_FORWARD ('slide-forward') — Next slide (→ or Space)
  • TRANSITION_BACK ('slide-back') — Previous slide (←)
The ViewTransition matches these types to animation names:
DirectionEnter AnimationExit Animation
Forwardslide-from-rightslide-to-left
Backwardslide-from-leftslide-to-right
Defaultslide-from-rightslide-to-left

CSS Animations

The CSS animations are defined in nextjs-slides/styles.css using View Transition pseudo-elements:
styles.css
/* Forward: new slide enters from right */
::view-transition-new(slide-from-right) {
  animation: slide-in-from-right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Forward: old slide exits to left */
::view-transition-old(slide-to-left) {
  animation: slide-out-to-left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Backward: new slide enters from left */
::view-transition-new(slide-from-left) {
  animation: slide-in-from-left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Backward: old slide exits to right */
::view-transition-old(slide-to-right) {
  animation: slide-out-to-right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes slide-in-from-right {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes slide-out-to-left {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

@keyframes slide-in-from-left {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes slide-out-to-right {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(100%);
    opacity: 0;
  }
}

Pseudo-Elements

  • ::view-transition-old(name) — The outgoing slide snapshot
  • ::view-transition-new(name) — The incoming slide snapshot
Both are rendered during the transition and removed when the animation completes.
Customize animations by overriding these keyframes or pseudo-element styles in your own CSS.

Exit Animation

The deck itself has an exit animation when navigating away (e.g. clicking the × button):
slide-deck.tsx
<ViewTransition default="none" exit="deck-unveil">
  <div id="slide-deck" className="...">
    {/* Deck content */}
  </div>
</ViewTransition>
The deck-unveil animation creates a shrinking effect:
styles.css
::view-transition-old(deck-unveil) {
  animation: deck-unveil 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes deck-unveil {
  from {
    transform: scale(1);
    opacity: 1;
  }
  to {
    transform: scale(0.95);
    opacity: 0;
  }
}
SlideDeck must be the direct child of the layout for the exit animation to work. Wrapping it in a <div> can prevent the ViewTransition from firing.

startTransition

SlideDeck uses React’s useTransition to wrap navigation:
slide-deck.tsx
const [isPending, startTransition] = useTransition();

startTransition(() => {
  addTransitionType(
    clamped > current ? TRANSITION_FORWARD : TRANSITION_BACK
  );
  router.push(`${basePath}/${targetSlide}`);
});
  • startTransition — Marks the navigation as a non-blocking transition
  • isPending — Tracks whether the transition is in progress
This prevents the UI from freezing during route changes and allows the ViewTransition to run smoothly.

Transition Lifecycle

  1. User presses arrow keykeydown listener triggers goTo()
  2. Add transition typeaddTransitionType('slide-forward') or addTransitionType('slide-back')
  3. Start transitionstartTransition() wraps router.push()
  4. Capture old state — Browser snapshots the current slide
  5. Update route — Next.js renders the new slide
  6. Capture new state — Browser snapshots the new slide
  7. Animate — CSS animations run on ::view-transition-old and ::view-transition-new
  8. Complete — Pseudo-elements are removed, new slide is live

Customizing Animations

Override the default animations by redefining the pseudo-element styles:
globals.css
/* Fade instead of slide */
::view-transition-new(slide-from-right),
::view-transition-new(slide-from-left) {
  animation: fade-in 0.3s ease-in-out;
}

::view-transition-old(slide-to-left),
::view-transition-old(slide-to-right) {
  animation: fade-out 0.3s ease-in-out;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

Change Duration

globals.css
::view-transition-new(slide-from-right),
::view-transition-old(slide-to-left) {
  animation-duration: 0.6s; /* Slower */
}

Change Easing

globals.css
::view-transition-new(slide-from-right),
::view-transition-old(slide-to-left) {
  animation-timing-function: ease-in-out;
}
Import nextjs-slides/styles.css in your global CSS to ensure the base animations are defined. Your overrides should come after.

Performance

View Transitions use hardware-accelerated transforms and run on the GPU:
  • transform: translateX() — GPU-accelerated
  • opacity — GPU-accelerated
  • No layout thrashing — Animations don’t trigger reflows
This ensures 60fps transitions even on low-end devices.

Browser Support

React 19 ViewTransitions require:
  • React 19<ViewTransition> component
  • Modern browsers — Chrome 111+, Edge 111+, Safari 18+ (with polyfill)
For older browsers, transitions degrade gracefully to instant route changes.
Use @supports (view-transition-name: auto) to detect View Transition support and provide fallback styles.

Build docs developers (and LLMs) love