Pinterest 过渡

卡片充满屏幕并扩展的沉浸式过渡

Pinterest 过渡

Pinterest 过渡是画廊中的小项目扩展到全屏的扩展过渡(Expansion Transition)。用户选择的内容充满屏幕并放大,提供强大的沉浸感和视觉连续性。

演示

Loading demo...

UX 原则

何时使用?

Pinterest 过渡用于视觉内容的放大浏览

内容关系适用性

콘텐츠 관계적합성설명
无关内容没有扩展连接点时不适用
兄弟关系⚠️画廊内项目之间移动时可考虑
层级关系最适合缩略图→全视图等同一内容的放大视图

主要用例

  • 图片画廊:从缩略图放大到高分辨率图片
  • 产品目录:从产品网格扩展到详细信息
  • 媒体浏览:从视频缩略图切换到播放器
  • 作品集:从作品列表到全屏演示

为什么这样工作?

  1. 空间扩展隐喻:通过小窗口进入大世界的感觉
  2. 自然焦点移动:用户视线自然跟随扩展元素
  3. 上下文保留:明确来源,保持方向感
  4. 戏剧效果:扩展动画强调内容的重要性

动效设计

扩展过程:
1. 捕获选定卡片的位置和大小
2. 卡片移动到屏幕中心并放大
3. 同时背景淡化和缩放变化
4. 最终填充整个屏幕

收缩过程:
1. 详情屏幕缩小并返回原位
2. 背景重新出现,画廊恢复
3. 自然的弹簧动画定位

基本用法

1. 过渡设置

import { Ssgoi } from '@ssgoi/react';
import { pinterest } from '@ssgoi/react/view-transitions';

const config = {
  transitions: [
    {
      from: '/gallery',
      to: '/gallery/*',
      transition: pinterest(),
      symmetric: true
    }
  ]
};

export default function App() {
  return (
    <Ssgoi config={config}>
      {/* 应用内容 */}
    </Ssgoi>
  );
}

2. 元素标记

画廊和详情页面分别使用不同的 data 属性:

// 画廊页面
function Gallery() {
  return (
    <div className="masonry-grid">
      {items.map(item => (
        <Link 
          key={item.id} 
          to={`/gallery/${item.id}`}
          data-pinterest-gallery-key={item.id}
          className="gallery-item"
        >
          <img src={item.image} alt={item.title} />
          <h3>{item.title}</h3>
        </Link>
      ))}
    </div>
  );
}

// 详情页面
function ItemDetail({ item }) {
  return (
    <div 
      data-pinterest-detail-key={item.id}
      className="detail-container"
    >
      <button onClick={() => navigate('/gallery')}>
        ✕ 关闭
      </button>
      <img src={item.image} alt={item.title} />
      <article>
        <h1>{item.title}</h1>
        <p>{item.description}</p>
      </article>
    </div>
  );
}

实践示例

1. 瀑布流画廊

Pinterest 风格瀑布流布局:

// 瀑布流网格
function MasonryGallery() {
  return (
    <div className="columns-2 md:columns-3 lg:columns-4 gap-4">
      {images.map((img, index) => (
        <div
          key={img.id}
          data-pinterest-gallery-key={img.id}
          onClick={() => navigate(`/image/${img.id}`)}
          className="break-inside-avoid mb-4 cursor-pointer 
                     hover:scale-105 transition-transform"
        >
          <img 
            src={img.url} 
            alt={img.title}
            className="w-full rounded-lg"
          />
          <div className="p-2">
            <p className="text-sm font-medium">{img.title}</p>
            <p className="text-xs text-gray-500">{img.author}</p>
          </div>
        </div>
      ))}
    </div>
  );
}

// 扩展视图
function ImageView({ image }) {
  return (
    <div 
      data-pinterest-detail-key={image.id}
      className="fixed inset-0 bg-white z-50 overflow-auto"
    >
      <div className="max-w-4xl mx-auto p-4">
        <img 
          src={image.url} 
          alt={image.title}
          className="w-full rounded-lg shadow-xl"
        />
        <div className="mt-4">
          <h1 className="text-2xl font-bold">{image.title}</h1>
          <p className="text-gray-600 mt-2">{image.description}</p>
        </div>
      </div>
    </div>
  );
}

2. 产品目录

产品卡片扩展到详情页面:

// 产品网格
function ProductGrid() {
  return (
    <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
      {products.map(product => (
        <article
          key={product.id}
          data-pinterest-gallery-key={product.id}
          onClick={() => navigate(`/product/${product.id}`)}
          className="bg-white rounded-xl shadow-lg overflow-hidden
                     cursor-pointer hover:shadow-xl transition-shadow"
        >
          <img 
            src={product.image} 
            alt={product.name}
            className="w-full h-48 object-cover"
          />
          <div className="p-4">
            <h3 className="font-semibold">{product.name}</h3>
            <p className="text-lg font-bold mt-2">${product.price}</p>
          </div>
        </article>
      ))}
    </div>
  );
}

// 产品详情
function ProductDetail({ product }) {
  return (
    <div 
      data-pinterest-detail-key={product.id}
      className="min-h-screen bg-white"
    >
      <div className="grid md:grid-cols-2 gap-8 p-8">
        <img 
          src={product.image} 
          alt={product.name}
          className="w-full rounded-lg"
        />
        <div>
          <h1 className="text-3xl font-bold">{product.name}</h1>
          <p className="text-2xl font-bold mt-4">${product.price}</p>
          <p className="mt-4 text-gray-600">{product.description}</p>
          <button className="mt-6 bg-black text-white px-8 py-3 rounded-lg">
            加入购物车
          </button>
        </div>
      </div>
    </div>
  );
}

自定义

弹簧设置

调整动画的弹性和速度:

pinterest({
  spring: {
    stiffness: 200,  // 越低越平滑
    damping: 25      // 越高越快稳定
  }
})

注意事项

键属性区分

  • 画廊data-pinterest-gallery-key
  • 详情data-pinterest-detail-key
  • 必须使用不同的属性名(与 Hero 区分)

布局考虑

  • 画廊项目必须是 position: static
  • 详情页面需要足够的边距
  • 与瀑布流布局特别匹配

无障碍性

  • 支持 ESC 键关闭详情页面
  • 焦点陷阱改善键盘导航
  • 减少动画设置时立即过渡

最佳实践

✅ 应该

  • 用于以图片为中心的内容
  • 应用于画廊或目录形式
  • 清晰提供关闭按钮
  • 在移动设备上也能良好工作的响应式设计

❌ 不应该

  • 不适合以文本为中心的内容
  • 避免太多元素同时扩展
  • 详情页面加载缓慢时避免使用
  • 复杂布局中可能出现意外行为