Skip to content

🎨 Page transition system for Nuxt 4 with GSAP & SplitText. Manual control via Vue directives (v-page-split, v-page-fade, v-page-clip, v-page-stagger). SSR-compatible, hardware-accelerated

Notifications You must be signed in to change notification settings

patsma/nuxt4page-transitions

Repository files navigation

Nuxt 4 Page Transitions with GSAP

A production-ready, directive-based page transition system for Nuxt 4 using GSAP and SplitText.

πŸš€ View Live Demo

Features

  • 🎯 Manual Control - No auto-detection, full control via Vue directives
  • 🎨 4 Animation Types - Split text, fade, clip-path, and stagger animations
  • ⚑ GSAP Powered - Hardware-accelerated, smooth 60fps animations
  • ✨ SplitText Integration - Character, word, and line reveals with masking
  • 🌊 ScrollSmoother - Buttery smooth scrolling with parallax effects (data-speed, data-lag)
  • πŸ”„ Opposite Animations - Elements animate OUT, then IN with opposite effects
  • 🎭 Simple & DRY - Two composables, 4 directives, zero complexity
  • πŸ“¦ Easy to Reuse - Copy 8 files and you're done

Demo

🎯 Try it live on Netlify

See the system in action with all 4 animation types. Navigate between pages to see smooth OUT/IN transitions.

Quick Example:

<!-- Page transitions + ScrollSmoother parallax -->
<h1 v-page-split:chars data-speed="0.8">Animated Title</h1>
<p v-page-fade:up data-speed="1.2">Fades up with faster scroll</p>
<div v-page-clip:top data-lag="0.2">Clips from top with smooth lag</div>
<ul v-page-stagger data-speed="0.9">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

Installation

1. Copy Files to Your Project

Copy these 8 files to your Nuxt 4 project:

your-project/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ composables/
β”‚   β”‚   β”œβ”€β”€ usePageTransition.js           # Page transition logic
β”‚   β”‚   └── useScrollSmootherManager.js    # ScrollSmoother manager
β”‚   β”œβ”€β”€ directives/
β”‚   β”‚   β”œβ”€β”€ v-page-split.js                # SplitText directive
β”‚   β”‚   β”œβ”€β”€ v-page-fade.js                 # Fade directive
β”‚   β”‚   β”œβ”€β”€ v-page-clip.js                 # Clip-path directive
β”‚   β”‚   └── v-page-stagger.js              # Stagger directive
β”‚   β”œβ”€β”€ plugins/
β”‚   β”‚   └── page-transitions.js            # Register directives (SSR-compatible)
β”‚   └── layouts/
β”‚       └── default.vue                    # Layout with ScrollSmoother + transitions

2. Install Dependencies

npm install gsap @hypernym/nuxt-gsap

3. Configure Nuxt

Add GSAP module to nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["@hypernym/nuxt-gsap"],

  gsap: {
    composables: true,
    provide: true,

    // Club GreenSock premium plugins (FREE as of 2025!)
    clubPlugins: {
      splitText: true, // Character/word/line text splitting
      scrollSmoother: true, // Smooth scrolling with parallax
    },

    // Extra plugins
    extraPlugins: {
      scrollTrigger: true, // Required for ScrollSmoother
    },
  },
});

4. Setup Layout

Create or update app/layouts/default.vue:

<script setup>
// Page transitions
const { leave, enter, beforeEnter, afterLeave } = usePageTransition();

// ScrollSmoother
const { createSmoother, killSmoother } = useScrollSmootherManager();

onMounted(() => {
  nextTick(() => {
    createSmoother({
      wrapper: "#smooth-wrapper",
      content: "#smooth-content",
      smooth: 1, // Optimized for consistent 60fps across all browsers
      effects: true, // Enable data-speed and data-lag
      normalizeScroll: true, // Improves performance, especially on Safari
      ignoreMobileResize: true, // Prevents janky resizing on mobile
    });
  });
});

onUnmounted(() => {
  killSmoother();
});
</script>

<template>
  <!-- ScrollSmoother wrapper -->
  <div id="smooth-wrapper">
    <div id="smooth-content">
      <div class="layout-wrapper">
        <!-- Your persistent nav/header here -->

        <!-- Page content with transitions -->
        <NuxtPage
          :transition="{
            name: 'page',
            mode: 'out-in',
            onBeforeEnter: beforeEnter,
            onEnter: enter,
            onLeave: leave,
            onAfterLeave: afterLeave,
          }"
        />
      </div>
    </div>
  </div>
</template>

5. Use Directives and Parallax in Pages

<template>
  <div class="page-content">
    <!-- Page transitions -->
    <h1 v-page-split:chars data-speed="0.8">My Page</h1>
    <p v-page-fade:up data-speed="1.2">Some text</p>

    <!-- Parallax effects -->
    <div v-page-clip:top data-lag="0.2" class="hero">
      <h2>Hero Section</h2>
    </div>

    <ul v-page-stagger data-speed="0.9">
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  </div>
</template>

Done! Navigate between pages to see animations + scroll to see parallax effects.

Directives Reference

v-page-split

Animates text using GSAP SplitText with character, word, or line splitting.

<!-- Arguments: :chars | :words | :lines -->
<h1 v-page-split:chars>Character reveal</h1>
<p v-page-split:words>Word by word</p>
<div v-page-split:lines>Line by line</div>

<!-- With custom config -->
<h1
  v-page-split:chars="{ stagger: 0.04, duration: 0.8, ease: 'back.out(1.5)' }"
>
  Custom timing
</h1>

Options:

  • splitType - 'chars', 'words', or 'lines'
  • stagger - Delay between each split element (default: 0.025)
  • duration - Animation duration (default: 0.6)
  • ease - GSAP easing function (default: 'back.out(1.5)')
  • y - Vertical movement distance (default: 35)

v-page-fade

Simple fade animation with optional directional movement.

<!-- Arguments: :up | :down | :left | :right -->
<div v-page-fade>Fade in (defaults to up)</div>
<p v-page-fade:up>Fade up</p>
<p v-page-fade:down>Fade down</p>
<div v-page-fade:left="{ distance: 40 }">Fade left with custom distance</div>

Options:

  • direction - 'up', 'down', 'left', 'right'
  • distance - Movement distance in pixels (default: 20)
  • duration - Animation duration (default: 0.6)
  • ease - GSAP easing function (default: 'power2.out')

v-page-clip

Modern clip-path reveal animations from any direction.

<!-- Arguments: :top | :bottom | :left | :right -->
<div v-page-clip>Clip from top (default)</div>
<div v-page-clip:top>Clip from top</div>
<div v-page-clip:bottom>Clip from bottom</div>
<div v-page-clip:left="{ duration: 0.8 }">Clip from left</div>

Options:

  • direction - 'top', 'bottom', 'left', 'right'
  • duration - Animation duration (default: 0.6)
  • ease - GSAP easing function (default: 'power2.out')

v-page-stagger

Stagger child elements with fade animation.

<ul v-page-stagger>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<!-- Custom config -->
<div v-page-stagger="{ stagger: 0.15, duration: 0.7 }">
  <div>Card 1</div>
  <div>Card 2</div>
</div>

<!-- Custom selector for nested elements -->
<nav v-page-stagger="{ selector: 'a' }">
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
</nav>

Options:

  • selector - CSS selector for children (default: ':scope > *')
  • stagger - Delay between each child (default: 0.1)
  • duration - Animation duration per child (default: 0.5)
  • ease - GSAP easing function (default: 'power2.out')

ScrollSmoother Parallax

Add data-speed and data-lag attributes to any element for smooth parallax effects.

data-speed

Controls how fast elements move relative to scroll:

<!-- Slower than scroll (background effect) -->
<div data-speed="0.5">Moves at 50% scroll speed</div>
<h1 data-speed="0.8">Moves at 80% scroll speed</h1>

<!-- Normal speed (default) -->
<p data-speed="1.0">Moves at 100% scroll speed</p>

<!-- Faster than scroll (foreground effect) -->
<div data-speed="1.5">Moves at 150% scroll speed</div>
<img data-speed="2.0">Moves at 200% scroll speed</img>

Values:

  • < 1.0 - Background effect (slower than scroll)
  • = 1.0 - Normal scroll speed (default)
  • > 1.0 - Foreground effect (faster than scroll)

data-lag

Creates a smooth "catch up" effect with momentum:

<!-- Subtle lag -->
<div data-lag="0.1">Slight trailing motion</div>

<!-- Medium lag -->
<h2 data-lag="0.2">Smooth catch-up effect</h2>

<!-- Heavy lag -->
<img data-lag="0.3">Pronounced trailing</img>

Values:

  • Typical range: 0.1 to 0.3
  • Higher values = more lag/trailing

Combining with Directives

Use both page transitions AND parallax on the same elements:

<h1 v-page-split:chars data-speed="0.8">
  Animated reveal + slow parallax
</h1>

<p v-page-fade:up data-lag="0.15">
  Fade transition + lag effect
</p>

<div v-page-clip:top data-speed="1.2">
  Clip animation + fast parallax
</div>

How It Works

Architecture

  1. Directives Store Config - When mounted, directives store animation config on elements:

    el._pageAnimation = { type: 'split', config: { stagger: 0.025, ... } }
  2. Page Transition Reads Config - During route changes, usePageTransition finds all elements with _pageAnimation property

  3. GSAP Timelines Execute - Animation functions build GSAP timelines for smooth OUT/IN animations

Animation Flow

User clicks link
    ↓
LEAVE hook fires
    ↓
Find elements with directives
    ↓
Animate all elements OUT (fade up, clip away, etc.)
    ↓
Old page removed
    ↓
New page mounted
    ↓
ENTER hook fires (with nextTick)
    ↓
Find elements with directives
    ↓
Animate all elements IN (opposite of OUT)
    ↓
Done!

Why OUT/IN Mode?

We use mode: 'out-in' so the old page completely animates out before the new page animates in. This creates a clean, professional transition without overlapping content.

Customization

Custom Easing

Use any GSAP easing function:

<h1 v-page-split:chars="{ ease: 'elastic.out(1, 0.3)' }">Bouncy</h1>
<p v-page-fade:up="{ ease: 'power4.out' }">Smooth</p>

Mix Animations

Use different directives on the same page:

<template>
  <div class="page-content">
    <h1 v-page-split:chars>Title</h1>
    <p v-page-fade:up>Subtitle</p>
    <div v-page-clip:top>Content box</div>
    <ul v-page-stagger>
      <li>List item 1</li>
      <li>List item 2</li>
    </ul>
  </div>
</template>

Different Animations Per Page

Each page can have completely different animations:

<!-- home.vue -->
<h1 v-page-split:chars>Home</h1>
<p v-page-fade:up>Welcome</p>

<!-- about.vue -->
<h1 v-page-split:words>About</h1>
<p v-page-clip:bottom>Different style</p>

Troubleshooting

Animations Not Working

  1. Check directives are registered:

    βœ… Page transition directives registered!
       - v-page-split:chars | :words | :lines
       - v-page-fade:in | :out | :up | :down | :left | :right
       - v-page-clip:top | :bottom | :left | :right
       - v-page-stagger:fade | :clip | :scale
    
  2. Check layout setup - Make sure usePageTransition is called in your layout

  3. Check page structure - Directives must be on direct children of .page-content

SplitText Not Working

Make sure SplitText is enabled in nuxt.config.ts:

gsap: {
  extraPlugins: {
    splitText: true;
  }
}

Elements Jump or Flicker

This happens if GSAP properties aren't being cleared. Make sure afterLeave hook is being called.

Only OUT Animation Works

The ENTER animation waits for nextTick() so directives have time to mount. If it's still not working, check console for "⚠️ No elements with page animation directives found".

Safari: Content Jumps During Page Transitions

Problem: On Safari (desktop and mobile), content jumps ~7px down when page transitions start.

Cause: SplitText with masking wraps each character/word/line in an overflow: hidden container, which adds ~7px to the element's height. Safari renders this layout shift visibly during the LEAVE transition.

Solution: The fix is built into animateSplit() in usePageTransition.js - it locks element height BEFORE SplitText runs:

// Lock height before SplitText to prevent Safari jump
const originalHeight = el.offsetHeight;
$gsap.set(el, { height: originalHeight });

// Now create split - can't grow because height is locked
const split = $SplitText.create(el, { type: splitType, mask: splitType });

This prevents the element from growing when masks are added. The locked height is automatically cleared in afterLeave() with clearProps: 'all'.

ScrollSmoother Effects Jump After Transition

Problem: Elements suddenly shift positions after page transitions.

Cause: Parallax effects are being applied after elements are visible.

Solution: The fix is already implemented in usePageTransition.js - effects are applied BEFORE animation:

// βœ… Correct timing (no jump)
refreshSmoother(); // Apply effects FIRST
const tl = $gsap.timeline({ onComplete: done });

If you still see jumps, verify that refreshSmoother() is called before creating the timeline.

ScrollSmoother Not Working

  1. Check nuxt.config.ts has both plugins enabled:
clubPlugins: {
  scrollSmoother: true;
}
extraPlugins: {
  scrollTrigger: true; // Required
}
  1. Verify wrapper elements in layout:
<div id="smooth-wrapper">
  <div id="smooth-content">
    <!-- Your content -->
  </div>
</div>

Performance

  • Hardware Accelerated - Uses CSS transforms and opacity for 60fps
  • Optimized for All Browsers - Settings tuned for consistent performance on Chrome, Safari, Firefox, and mobile
  • Safari Optimized - Built-in fix for SplitText height jump issue
  • Automatic Cleanup - SplitText instances are reverted after animations
  • GSAP Timeline - Efficient timeline-based animation system
  • Production Ready - Battle-tested animation patterns

ScrollSmoother Performance Settings

The system uses optimized ScrollSmoother settings for consistent 60fps across all browsers:

{
  smooth: 1,                    // Lower value = better performance
  effects: true,                // Enable data-speed and data-lag
  normalizeScroll: true,        // Significantly improves Safari performance
  ignoreMobileResize: true      // Prevents mobile jank
}

Why smooth: 1?

  • Higher values (like 2 or 3) look smoother but can drop to 14fps on Safari
  • smooth: 1 maintains consistent 60fps across all browsers
  • Still provides the signature "buttery smooth" ScrollSmoother feel

Safari-Specific Issues Solved:

  1. βœ… Content jump during transitions (SplitText height lock fix)
  2. βœ… Slow 14fps scrolling (optimized smooth value + normalizeScroll)
  3. βœ… Parallax effects not working after route change (refreshSmoother timing fix)

Browser Support

Works in all modern browsers that support:

  • CSS clip-path
  • CSS transforms
  • GSAP 3.x

Links

License

MIT - Use it however you want!

Credits

Built with:

  • GSAP - Industry-standard animation library
  • Nuxt 4 - Vue framework
  • SplitText - Premium GSAP plugin (FREE as of 2025)
  • ScrollSmoother - Premium GSAP plugin (FREE as of 2025)

About

🎨 Page transition system for Nuxt 4 with GSAP & SplitText. Manual control via Vue directives (v-page-split, v-page-fade, v-page-clip, v-page-stagger). SSR-compatible, hardware-accelerated

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published