AnimationsSimpleApril 8, 2026

View Transitions API

Old page scales down and slides up off screen. New page slides in from below. Uses the browser's native View Transitions API with document.startViewTransition(). No overlay components needed.

View Full Demo →

Home

Uses the native View Transitions API

index.jsx
"use client";

import { useState, useCallback } from "react";
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" },
];

export default function ViewTransitionsApi() {
  const [pageIndex, setPageIndex] = useState(0);

  const navigate = useCallback((nextIndex) => {
    if (nextIndex === pageIndex) return;

    if (!document.startViewTransition) {
      setPageIndex(nextIndex);
      return;
    }

    document.startViewTransition(() => {
      setPageIndex(nextIndex);
    });
  }, [pageIndex]);

  const page = PAGES[pageIndex];

  return (
    <div className={styles.demo}>
      <div
        className={styles.page}
        style={{ background: page.bg, color: page.color }}
      >
        <h2 className={styles.pageTitle}>{page.label}</h2>
        <p className={styles.hint}>Uses the native View Transitions API</p>
      </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;
}

.page {
  height: 320px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  view-transition-name: page;
}

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

.hint {
  font-size: 0.8125rem;
  opacity: 0.4;
}

.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;
}

/* View Transitions API styles */
@keyframes slide-out-up {
  from { opacity: 1; scale: 1; transform: translateY(0); }
  to { opacity: 0.5; scale: 0.95; transform: translateY(-80px); }
}

@keyframes slide-in-from-below {
  from { opacity: 0; transform: translateY(40px); }
  to { opacity: 1; transform: translateY(0); }
}

:global(::view-transition-old(root)) {
  animation: slide-out-up 600ms cubic-bezier(0.87, 0, 0.13, 1) both;
}

:global(::view-transition-new(root)) {
  animation: slide-in-from-below 600ms cubic-bezier(0.87, 0, 0.13, 1) both;
}

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.