AnimationsIntermediateApril 8, 2026

Inner Perspective Transition

A slide panel covers the screen while the current page scales back and fades, creating a depth/perspective effect. Three simultaneous Motion animations: slide overlay, page scale-back, and opacity fade on enter.

View Full Demo →

Home

index.jsx
"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import styles from "./styles.module.css";

const PAGES = [
  { key: "home", label: "Home", bg: "#f5f5f5", color: "#1a1a1a" },
  { key: "about", label: "About", bg: "#0d0d0d", color: "#ffffff" },
  { key: "work", label: "Work", bg: "#1a1f2e", color: "#ffffff" },
];

const slideVariants = {
  initial: { top: "-100vh" },
  enter: { top: "-100vh" },
  exit: { top: 0, transition: { duration: 0.6, ease: [0.76, 0, 0.24, 1] } },
};

const perspectiveVariants = {
  initial: { scale: 1, y: 0, opacity: 1 },
  exit: { scale: 0.9, y: -150, opacity: 0.5, transition: { duration: 0.6, ease: [0.76, 0, 0.24, 1] } },
};

const opacityVariants = {
  initial: { opacity: 0 },
  enter: { opacity: 1, transition: { duration: 0.5, delay: 0.2 } },
};

export default function InnerPerspectiveTransition() {
  const [pageIndex, setPageIndex] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);

  const page = PAGES[pageIndex];

  function navigate(nextIndex) {
    if (isAnimating || nextIndex === pageIndex) return;
    setIsAnimating(true);
    setPageIndex(nextIndex);
    setTimeout(() => setIsAnimating(false), 800);
  }

  return (
    <div className={styles.demo}>
      <div className={styles.viewport}>
        {/* Slide panel that covers screen */}
        <AnimatePresence>
          <motion.div
            key={`slide-${pageIndex}`}
            className={styles.slide}
            variants={slideVariants}
            initial="initial"
            animate="enter"
            exit="exit"
          />
        </AnimatePresence>

        {/* Current page scales back on exit */}
        <AnimatePresence mode="wait">
          <motion.div
            key={`page-${pageIndex}`}
            className={styles.page}
            style={{ background: page.bg, color: page.color }}
            variants={perspectiveVariants}
            initial="initial"
            exit="exit"
          >
            <motion.div variants={opacityVariants} initial="initial" animate="enter">
              <h2 className={styles.pageTitle}>{page.label}</h2>
            </motion.div>
          </motion.div>
        </AnimatePresence>
      </div>

      <div className={styles.nav}>
        {PAGES.map((p, i) => (
          <button
            key={p.key}
            className={`${styles.navBtn} ${i === pageIndex ? styles.active : ""}`}
            onClick={() => navigate(i)}
          >
            {p.label}
          </button>
        ))}
      </div>
    </div>
  );
}
styles.module.css
.demo {
  display: flex;
  flex-direction: column;
}

.viewport {
  position: relative;
  height: 320px;
  overflow: hidden;
}

.slide {
  position: absolute;
  top: -100%;
  left: 0;
  width: 100%;
  height: 100%;
  background: #1a1a1a;
  z-index: 10;
}

.page {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-origin: center top;
}

.pageTitle {
  font-size: 2.5rem;
  font-weight: 700;
  letter-spacing: -0.03em;
}

.nav {
  display: flex;
  gap: 1px;
  background: #e5e5e5;
  border-top: 1px solid #e5e5e5;
}

.navBtn {
  flex: 1;
  padding: 0.875rem;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: inherit;
  background: #ffffff;
  border: none;
  cursor: pointer;
  color: #6b6b6b;
  transition: background 150ms ease, color 150ms ease;
}

.navBtn:hover {
  background: #f5f5f5;
  color: #1a1a1a;
}

.navBtn.active {
  background: #1a1a1a;
  color: #ffffff;
}
  • framer-motion

Apr 27, 2026

ANIMATIONS

Cursor Hover Label

A custom cursor label that follows the mouse and fades in when hovering trigger elements. Uses GSAP quickTo for smooth tracking. Trigger elements use data-cursor-label attributes to set label text. Inspired by portfolio/agency sites like Studio PIC.

Apr 27, 2026

ANIMATIONS

Section Transition 01

GSAP ScrollTrigger-based section transition system. Sibling sections opt into parallax, pin, or reveal modes via data attributes. Supports y offset, overlay opacity, and overlay color per section. Mobile strategy simplifies motion on smaller screens.

Apr 27, 2026

ANIMATIONS

Text Reveal 01

GSAP SplitText-based text reveal system. Text elements opt in with data-reveal-01 and split into lines, words, or characters. Supports load-time reveals, scroll-triggered reveals, scrubbed scroll reveals, and manual split-only mode for custom timelines. Per-element overrides for duration, stagger, delay, ease, and replay behavior.