Sheet 过渡
底部弹出式移动端优化过渡
Sheet 过渡
Sheet 过渡是一种底部弹出式的移动端优化过渡。背景页面会向后退并缩小,新屏幕从底部向上滑动出现。常用于从 FAB 按钮切换到编辑屏幕时。
演示
Loading demo...
UX 原则
何时使用?
Sheet 过渡用于从 FAB 或编辑按钮切换到全屏输入模式。
按内容关系的适用性
| 콘텐츠 관계 | 적합성 | 설명 |
|---|---|---|
| 无关内容 | ❌ | 不适用于独立部分之间 |
| 同级关系 | ❌ | 同级页面间移动建议使用滑动 |
| 层级关系 | ✅ | 最适合创建新内容场景,如列表→编辑、动态→发帖等 |
主要用例
- 社交媒体: 从动态发布新帖子
- 邮件应用: 从收件箱撰写新邮件
- 笔记应用: 从笔记列表创建新笔记
- 待办事项应用: 从列表添加新任务
- 聊天应用: 从对话列表撰写新消息
为什么这样运作?
- 保留上下文: 背景缩小并保持可见,让用户清楚知道来自哪里
- 空间扩展隐喻: 新的工作空间从下方升起的自然感觉
- 引导专注: 背景页面模糊并缩小,聚焦于编辑屏幕
- 移动原生模式: 类似 iOS 和 Android 原生应用的模态过渡
- 轻松返回: 返回时背景再次放大,自然回归
动效设计
前进过程(列表 → 编辑):
1. 背景页面开始缩小 (scale: 1 → 0.8)
2. 同时降低透明度 (opacity: 1 → 0)
3. 编辑页面从底部向上滑动 (translateY: 100% → 0)
4. 背景设置为 z-index: -1 置于后方
5. 编辑页面设置为 z-index: 100 置于前方
后退过程(编辑 → 列表):
1. 编辑页面向下滑动至底部 (translateY: 0 → 100%)
2. 同时背景页面放大 (scale: 0.8 → 1)
3. 恢复背景透明度 (opacity: 0 → 1)
4. 中心点固定在视口中心
基本用法
1. 过渡配置
Sheet 过渡使用 direction 选项明确设置前进/后退方向:
import { Ssgoi } from '@ssgoi/react';
import { sheet } from '@ssgoi/react/view-transitions';
const config = {
transitions: [
{
from: '/feed',
to: '/compose',
transition: sheet({ direction: 'enter' }) // 前进: sheet 上升
},
{
from: '/compose',
to: '/feed',
transition: sheet({ direction: 'exit' }) // 后退: sheet 下降
}
]
};
export default function App() {
return (
<Ssgoi config={config}>
{/* 应用内容 */}
</Ssgoi>
);
}
2. 页面结构
带有 FAB 按钮的列表页面和编辑页面:
// 动态页面
function Feed() {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
<header className="p-4 border-b border-gray-700">
<h1 className="text-xl font-bold">动态</h1>
</header>
<main className="p-4 space-y-4">
{posts.map(post => (
<article
key={post.id}
className="bg-gray-800 rounded-lg p-4"
>
<h2 className="font-semibold">{post.title}</h2>
<p className="text-gray-400 text-sm mt-2">{post.excerpt}</p>
</article>
))}
</main>
{/* FAB */}
<button
onClick={() => navigate('/compose')}
className="fixed bottom-6 right-6 w-14 h-14
bg-blue-500 rounded-full shadow-lg
flex items-center justify-center
hover:bg-blue-600 transition-colors"
>
<span className="text-2xl text-white">+</span>
</button>
</div>
);
}
// 编辑页面
function Compose() {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
<header className="p-4 border-b border-gray-700 flex items-center gap-4">
<button
onClick={() => navigate(-1)}
className="text-gray-400 hover:text-gray-200"
>
取消
</button>
<h1 className="text-xl font-bold flex-1">新帖子</h1>
<button className="text-blue-500 font-semibold">
发布
</button>
</header>
<main className="p-4">
<input
type="text"
placeholder="标题"
className="w-full bg-gray-800 border border-gray-700
rounded-lg px-4 py-3 mb-4"
/>
<textarea
placeholder="输入内容..."
rows={10}
className="w-full bg-gray-800 border border-gray-700
rounded-lg px-4 py-3 resize-none"
/>
</main>
</div>
);
}
实际应用示例
1. 社交媒体发帖
Twitter/X 风格的新帖子发布:
// 时间线页面
function Timeline() {
return (
<div className="min-h-screen bg-gray-900">
<header className="sticky top-0 bg-gray-900/95 backdrop-blur p-4
border-b border-gray-700">
<h1 className="text-xl font-bold text-gray-100">时间线</h1>
</header>
<main>
{tweets.map(tweet => (
<article
key={tweet.id}
className="p-4 border-b border-gray-800 hover:bg-gray-800/50"
>
<div className="flex gap-3">
<img
src={tweet.avatar}
alt={tweet.author}
className="w-12 h-12 rounded-full"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-gray-100">
{tweet.author}
</span>
<span className="text-gray-500 text-sm">
@{tweet.handle}
</span>
<span className="text-gray-500 text-sm">
· {tweet.time}
</span>
</div>
<p className="text-gray-200 mt-1">{tweet.content}</p>
</div>
</div>
</article>
))}
</main>
{/* Compose FAB */}
<Link
to="/compose"
className="fixed bottom-6 right-6 w-14 h-14
bg-blue-500 rounded-full shadow-lg
flex items-center justify-center
hover:bg-blue-600 active:scale-95
transition-all"
>
<svg className="w-6 h-6 text-white" fill="currentColor">
<path d="M8 2v12m-6-6h12" stroke="currentColor" strokeWidth="2"/>
</svg>
</Link>
</div>
);
}
// 编辑页面
function ComposeTweet() {
const [content, setContent] = useState('');
const navigate = useNavigate();
const handlePost = () => {
// 发布逻辑
navigate(-1);
};
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
<header className="p-4 border-b border-gray-700
flex items-center justify-between">
<button
onClick={() => navigate(-1)}
className="text-gray-400 hover:text-gray-200 font-medium"
>
取消
</button>
<button
onClick={handlePost}
disabled={!content.trim()}
className="bg-blue-500 text-white px-4 py-2 rounded-full
font-semibold disabled:opacity-50
disabled:cursor-not-allowed"
>
发布
</button>
</header>
<main className="p-4">
<div className="flex gap-3">
<img
src="/current-user-avatar.jpg"
alt="You"
className="w-12 h-12 rounded-full"
/>
<textarea
autoFocus
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="有什么新鲜事?"
className="flex-1 bg-transparent text-xl
text-gray-100 placeholder-gray-500
resize-none outline-none min-h-[200px]"
/>
</div>
{/* 字符计数器 */}
<div className="mt-4 flex items-center justify-between
px-4 py-2 border-t border-gray-700">
<div className="flex gap-2">
<button className="text-blue-500 p-2 hover:bg-blue-500/10
rounded-full">
<span>📷</span>
</button>
<button className="text-blue-500 p-2 hover:bg-blue-500/10
rounded-full">
<span>😊</span>
</button>
</div>
<span className={`text-sm ${content.length > 280 ? 'text-red-500' : 'text-gray-500'}`}>
{content.length} / 280
</span>
</div>
</main>
</div>
);
}
2. 邮件编辑
从收件箱撰写新邮件:
// 收件箱
function Inbox() {
return (
<div className="min-h-screen bg-gray-900">
<header className="p-4 border-b border-gray-700">
<h1 className="text-xl font-bold text-gray-100">收件箱</h1>
</header>
<main>
{emails.map(email => (
<article
key={email.id}
className="p-4 border-b border-gray-800
hover:bg-gray-800/50 cursor-pointer"
>
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-full bg-blue-500
flex items-center justify-center
text-white font-semibold">
{email.sender[0]}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-gray-100">
{email.sender}
</span>
<span className="text-xs text-gray-500">
{email.time}
</span>
</div>
<h3 className="text-gray-100 font-medium mb-1 truncate">
{email.subject}
</h3>
<p className="text-gray-400 text-sm truncate">
{email.preview}
</p>
</div>
</div>
</article>
))}
</main>
{/* Compose FAB */}
<Link
to="/compose-email"
className="fixed bottom-6 right-6 w-14 h-14
bg-blue-500 rounded-full shadow-lg
flex items-center justify-center"
>
<span className="text-white text-2xl">✉️</span>
</Link>
</div>
);
}
// 邮件编辑
function ComposeEmail() {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-900 text-gray-100">
<header className="p-4 border-b border-gray-700
flex items-center justify-between">
<button onClick={() => navigate(-1)}>取消</button>
<h1 className="text-lg font-semibold">新邮件</h1>
<button className="text-blue-500 font-semibold">发送</button>
</header>
<main className="divide-y divide-gray-800">
<input
type="email"
placeholder="收件人"
className="w-full bg-transparent px-4 py-3
text-gray-100 placeholder-gray-500"
/>
<input
type="text"
placeholder="主题"
className="w-full bg-transparent px-4 py-3
text-gray-100 placeholder-gray-500"
/>
<textarea
placeholder="邮件内容"
rows={15}
className="w-full bg-transparent px-4 py-3
text-gray-100 placeholder-gray-500
resize-none outline-none"
/>
</main>
</div>
);
}
自定义
direction 选项
指定 sheet 过渡的方向:
// 前进方向(列表 → 编辑): sheet 从下方升起
sheet({ direction: 'enter' })
// 后退方向(编辑 → 列表): sheet 向下降落
sheet({ direction: 'exit' })
重要: direction: 'enter' 和 direction: 'exit' 的 z-index 处理不同:
enter: 进入的 sheet 自然显示在前面exit: 退出的 sheet 保持在z-index: 100前面并下降
弹簧设置
调整动画的弹性和速度:
sheet({
direction: 'enter',
physics: {
spring: {
stiffness: 140, // 越低越平滑(默认: 140)
damping: 18, // 越高越快稳定(默认: 18)
doubleSpring: 0.5 // OUT 动画的弹簧强度倍数(默认: 0.5)
}
}
})
平滑过渡
更平滑、更从容的过渡:
sheet({
physics: {
spring: {
stiffness: 100,
damping: 20,
doubleSpring: 0.6
}
}
})
快速过渡
更快速、更响应的过渡:
sheet({
physics: {
spring: {
stiffness: 180,
damping: 16,
doubleSpring: 0.4
}
}
})
注意事项
性能优化
- 使用 GPU 加速(使用
transform、opacity) - 自动设置和取消
will-change - 使用
backfaceVisibility: hidden防止闪烁 - 避免布局重新计算
z-index 管理
- 编辑页面:
z-index: 100(显示在前面) - 背景页面:
z-index: -1(置于后方) - 自动管理,无需手动设置
移动端优化
- 为移动设备优化的时序
- 与触摸手势自然配合
- 保证 60fps 流畅动画
无障碍访问
- 在
prefers-reduced-motion设置时禁用过渡 - 完全支持键盘导航
- 支持 ESC 键关闭编辑页面
- 焦点陷阱改善键盘可访问性
最佳实践
✅ 应该
- 从 FAB 按钮切换到全屏编辑模式时使用
- 在社交媒体、邮件、笔记等新内容创建时应用
- 同时设置
direction: 'enter'和direction: 'exit'保持双向一致性 - 编辑页面使用全屏布局
- 清晰提供取消和完成按钮
❌ 不应该
- 不要用于常规页面间导航
- 不适合只读内容查看(使用 instagram 或 hero)
- 不要用于标签导航(使用 slide)
- 不适合没有层级结构的页面间移动
- 桌面环境考虑使用模态框或其他过渡
相关过渡
- Drill: 适合层级内容导航
- Slide: 适合水平标签导航
- Instagram: 适合图片放大查看
- Fade: 适合一般页面过渡
浏览器兼容性
- Chrome/Edge(最新版)
- Firefox(最新版)
- Safari(最新版)
- 所有现代移动浏览器