You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Next.js中实现缩略图转全屏图片的页面过渡效果

Next.js 实现页面跳转时图片过渡效果

核心思路

  1. 首页点击缩略图时,记录当前图片的位置、尺寸、src,通过路由传递给详情页
  2. 利用Next.js路由API监听切换状态,控制过渡层的显示/隐藏
  3. 在过渡层中完成从缩略图到详情页头部的平滑动画,动画结束后销毁过渡层

具体实现步骤

1. 首页(列表页)组件

import { useRouter } from 'next/router';
import { useRef, useState } from 'react';

// 模拟项目数据
const projects = [
  { id: 1, title: '项目A', imgSrc: '/images/project-a.jpg' },
  { id: 2, title: '项目B', imgSrc: '/images/project-b.jpg' }
];

export default function Home() {
  const router = useRouter();
  const [transitionImg, setTransitionImg] = useState(null);
  const imgRefs = useRef({});

  const handleProjectClick = (e, project) => {
    // 获取当前图片的DOM位置信息
    const imgEl = imgRefs.current[project.id];
    const rect = imgEl.getBoundingClientRect();
    
    // 存储过渡所需的图片数据
    setTransitionImg({
      src: project.imgSrc,
      width: rect.width,
      height: rect.height,
      top: rect.top + window.scrollY,
      left: rect.left,
      id: project.id
    });

    // 延迟跳转,确保过渡层已渲染
    setTimeout(() => {
      router.push({
        pathname: `/projects/${project.id}`,
        query: { imgSrc: project.imgSrc }
      });
    }, 50);
  };

  return (
    <div className="project-list">
      {projects.map(project => (
        <div 
          key={project.id} 
          className="project-item"
          onClick={(e) => handleProjectClick(e, project)}
        >
          <img
            ref={el => imgRefs.current[project.id] = el}
            src={project.imgSrc}
            alt={project.title}
            className="project-thumb"
          />
          <h3>{project.title}</h3>
        </div>
      ))}

      {/* 过渡层 - 跳转时显示 */}
      {transitionImg && (
        <div className="transition-overlay">
          <img
            src={transitionImg.src}
            alt="过渡图片"
            className="transition-img"
            style={{
              position: 'fixed',
              top: transitionImg.top,
              left: transitionImg.left,
              width: transitionImg.width,
              height: transitionImg.height,
              objectFit: 'cover',
              zIndex: 9999
            }}
          />
        </div>
      )}
    </div>
  );
}

2. 详情页组件

import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';

export default function ProjectDetail() {
  const router = useRouter();
  const { imgSrc } = router.query;
  const headerImgRef = useRef(null);
  const [isTransitioning, setIsTransitioning] = useState(true);

  useEffect(() => {
    if (!isTransitioning) return;

    // 获取详情页头部图片的最终位置
    const headerRect = headerImgRef.current.getBoundingClientRect();
    const transitionImg = document.querySelector('.transition-img');
    
    if (!transitionImg) {
      setIsTransitioning(false);
      return;
    }

    // 触发CSS过渡动画
    transitionImg.style.transition = 'all 0.6s ease-out';
    transitionImg.style.top = `${headerRect.top + window.scrollY}px`;
    transitionImg.style.left = `${headerRect.left}px`;
    transitionImg.style.width = `${headerRect.width}px`;
    transitionImg.style.height = `${headerRect.height}px`;

    // 动画结束后清理过渡层
    const handleTransitionEnd = () => {
      transitionImg.remove();
      setIsTransitioning(false);
      transitionImg.removeEventListener('transitionend', handleTransitionEnd);
    };
    transitionImg.addEventListener('transitionend', handleTransitionEnd);

    // 监听浏览器后退,处理返回动画
    const handlePopState = () => {
      setIsTransitioning(true);
      // 可在此添加返回首页时的反向动画逻辑
    };
    router.beforePopState(handlePopState);

    return () => {
      router.beforePopState(() => true); // 清除监听
    };
  }, [isTransitioning, router]);

  return (
    <div className="project-detail">
      {/* 详情页头部图片 */}
      <div className="detail-header">
        <img
          ref={headerImgRef}
          src={imgSrc}
          alt="项目头部图"
          className="header-img"
          style={{ opacity: isTransitioning ? 0 : 1 }}
        />
      </div>
      {/* 项目详情内容 */}
      <div className="detail-content">
        <h1>项目详情</h1>
        <p>这里是项目的具体内容...</p>
      </div>
    </div>
  );
}

3. 全局CSS补充

.transition-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  z-index: 9999;
}

.project-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 2rem;
  padding: 2rem;
}

.project-thumb {
  width: 100%;
  height: 200px;
  object-fit: cover;
  cursor: pointer;
  transition: transform 0.2s ease;
}

.project-thumb:hover {
  transform: scale(1.05);
}

.header-img {
  width: 100%;
  height: 60vh;
  object-fit: cover;
  transition: opacity 0.3s ease;
}

React Router 适配方案

核心逻辑和Next.js一致,仅替换路由API为React Router的useNavigate和useLocation

1. 首页组件

import { useNavigate } from 'react-router-dom';
import { useRef, useState } from 'react';

const projects = [
  { id: 1, title: '项目A', imgSrc: '/images/project-a.jpg' },
  { id: 2, title: '项目B', imgSrc: '/images/project-b.jpg' }
];

export default function Home() {
  const navigate = useNavigate();
  const [transitionImg, setTransitionImg] = useState(null);
  const imgRefs = useRef({});

  const handleProjectClick = (e, project) => {
    const imgEl = imgRefs.current[project.id];
    const rect = imgEl.getBoundingClientRect();
    
    setTransitionImg({
      src: project.imgSrc,
      width: rect.width,
      height: rect.height,
      top: rect.top + window.scrollY,
      left: rect.left,
      id: project.id
    });

    setTimeout(() => {
      navigate(`/projects/${project.id}`, {
        state: { imgSrc: project.imgSrc, rect }
      });
    }, 50);
  };

  // 其余结构和Next.js首页一致,过渡层写法相同
}

2. 详情页组件

import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';

export default function ProjectDetail() {
  const location = useLocation();
  const { imgSrc } = location.state || {};
  const headerImgRef = useRef(null);
  const [isTransitioning, setIsTransitioning] = useState(true);

  useEffect(() => {
    if (!isTransitioning || !imgSrc) return;

    const headerRect = headerImgRef.current.getBoundingClientRect();
    const transitionImg = document.querySelector('.transition-img');
    
    if (!transitionImg) {
      setIsTransitioning(false);
      return;
    }

    transitionImg.style.transition = 'all 0.6s ease-out';
    transitionImg.style.top = `${headerRect.top + window.scrollY}px`;
    transitionImg.style.left = `${headerRect.left}px`;
    transitionImg.style.width = `${headerRect.width}px`;
    transitionImg.style.height = `${headerRect.height}px`;

    const handleTransitionEnd = () => {
      transitionImg.remove();
      setIsTransitioning(false);
      transitionImg.removeEventListener('transitionend', handleTransitionEnd);
    };
    transitionImg.addEventListener('transitionend', handleTransitionEnd);

    // 监听浏览器后退
    const handlePopState = () => {
      setIsTransitioning(true);
      // 可添加返回时的反向动画
    };
    window.addEventListener('popstate', handlePopState);

    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, [isTransitioning, imgSrc]);

  // 其余结构和Next.js详情页一致
}

关键注意事项

  • 图片预加载:点击缩略图时提前预加载详情页大图,避免动画过程中图片加载不全
  • 响应式适配:处理不同屏幕尺寸下的位置计算,防止动画错位
  • 路由状态稳定性:Next.js用query传递数据更稳定,刷新页面不会丢失;React Router用state时需添加刷新后的 fallback 逻辑
  • 动画性能:优先用transform替代top/left属性,减少页面重排提升流畅度

内容的提问来源于stack exchange,提问作者Vansh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 10:48:19