シート(Sheet)トランジション
ボトムシートスタイルのモバイル最適化トランジション
シート(Sheet)トランジション
シート(Sheet)トランジションは、ボトムシートスタイルのモバイル最適化トランジションです。バックグラウンドページは後ろに下がりながら縮小し、新しい画面が下部からスライドアップして表示されます。FABボタンから作成画面への遷移時によく使用されます。
デモ
Loading demo...
UX原則
いつ使用しますか?
シート(Sheet)トランジションは、FABまたは作成ボタンから全画面入力モードへの遷移時に使用されます。
コンテンツ関係別適合性
| 콘텐츠 관계 | 적합성 | 설명 |
|---|---|---|
| 関連のないコンテンツ | ❌ | 独立したセクション間には不適合 |
| 兄弟関係 | ❌ | 同レベルのページ間の移動にはスライドの使用を推奨 |
| 階層関係 | ✅ | リスト→作成、フィード→投稿作成など新しいコンテキスト生成時に最適 |
主な使用ケース
- ソーシャルメディア: フィードから新しい投稿を作成
- メールアプリ: 受信トレイから新しいメールを作成
- メモアプリ: ノートリストから新しいノートを作成
- ToDoアプリ: リストから新しいタスクを追加
- チャットアプリ: 会話リストから新しいメッセージを作成
なぜこのように動作しますか?
- コンテキストの保存: バックグラウンドが縮小しながら維持され、ユーザーがどこから来たかが明確
- 空間拡張メタファー: 新しい作業スペースが下から上がってくる自然な感覚
- 集中の誘導: バックグラウンドページがぼやけて縮小し、作成画面に集中
- モバイルネイティブパターン: 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. トランジション設定
シートトランジションはdirectionオプションで前進/後退方向を明示的に設定します:
import { Ssgoi } from '@ssgoi/react';
import { sheet } from '@ssgoi/react/view-transitions';
const config = {
transitions: [
{
from: '/feed',
to: '/compose',
transition: sheet({ direction: 'enter' }) // 前進: シートが上がる
},
{
from: '/compose',
to: '/feed',
transition: sheet({ direction: 'exit' }) // 後退: シートが下がる
}
]
};
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({ direction: 'enter' })
// 後退方向 (作成 → リスト): シートが下に下がる
sheet({ direction: 'exit' })
重要: direction: 'enter'とdirection: 'exit'はz-index処理が異なります:
enter: 進入するシートが自然に前面に表示exit: 退出するシートが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キーで作成ページを閉じる対応
- フォーカストラップでキーボードアクセシビリティ向上
ベストプラクティス
✅ DO
- FABボタンから全画面作成モードへの遷移時に使用
- ソーシャルメディア、メール、メモなど新しいコンテンツ作成時に適用
direction: 'enter'とdirection: 'exit'を一緒に設定して双方向の一貫性を維持- 作成ページは全画面レイアウトを使用
- キャンセルおよび完了ボタンを明確に提供
❌ DON'T
- 一般的なページ間ナビゲーションには使用しないでください
- 読み取り専用コンテンツの表示には不適合 (InstagramまたはHeroを使用)
- タブナビゲーションには使用しないでください (スライドを使用)
- 階層構造のないページ間の移動には不適合
- デスクトップ環境ではモーダルまたは他のトランジションを検討
関連トランジション
- ドリル: 階層的なコンテンツナビゲーションに適合
- スライド: 水平タブナビゲーションに適合
- Instagram: 画像拡大表示に適合
- フェード: 一般的なページトランジションに適合
ブラウザ互換性
- Chrome/Edge (最新)
- Firefox (最新)
- Safari (最新)
- すべてのモダンモバイルブラウザ