Message Enter Animation — View Transition API
Khi send message mới, old messages cross-fade OUT đồng thời new content slide UP — giống ChatGPT.
Visual
PHASE 1: Capture OLD PHASE 2: Capture NEW
┌──────────────────┐ ┌──────────────────┐
│ Msg 3 │ ← screenshot │ 👤 New message │ ← screenshot
│ Msg 4 │ │ │
│ Msg 5 │ │ ⚛ Nebula │
│ │ │ ●●● │
├──────────────────┤ │ (khoảng trống) │
│ [input: "hello"] │ ├──────────────────┤
└──────────────────┘ │ [input: empty] │
└──────────────────┘
PHASE 3: Animate OLD → NEW (đồng thời, 400ms)
┌──────────────────┐
│ ╔═ OLD ════════╗ │ cross-fade OUT (opacity 1→0)
│ ║ Msg 3-5 ║ │
│ ╚══════════════╝ │
│ ╔═ NEW ════════╗ │ slide UP + fade IN (translateY 20vh→0)
│ ║ 👤 New msg ║ │
│ ║ ⚛ ●●● ║ │
│ ╚══════════════╝ │
└──────────────────┘
Cách View Transition hoạt động
document.startViewTransition(() => {
// 1. Browser chụp SCREENSHOT trạng thái CŨ
// 2. Callback chạy: DOM update (addMessage, setPlaceholder, scrollIntoView)
// 3. Browser chụp SCREENSHOT trạng thái MỚI
// 4. Browser animate: old screenshot → new screenshot (đồng thời)
})
Browser handle animation bằng 2 pseudo-elements:
::view-transition-old(root)— screenshot cũ, cross-fade OUT::view-transition-new(root)— screenshot mới, slide UP + fade IN
CSS
@keyframes slide-up {
from {
opacity: 0;
translate: 0 20vh;
}
to {
opacity: 1;
translate: 0 0;
}
}
::view-transition-old(root) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
::view-transition-new(root) {
animation: slide-up 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Fallback: CSS class khi View Transition không support */
.message-enter {
animation: slide-up 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
React
function handleSend(text) {
const doUpdate = () => {
appendBlock({ type: 'user', content: text });
setPlaceholderHeight(calcHeight());
startStreaming();
};
// View Transition API: old → new animation
if (document.startViewTransition) {
document.startViewTransition(doUpdate);
} else {
doUpdate(); // fallback: no animation
}
}
Flutter
Flutter không có View Transition API. Dùng AnimatedSwitcher hoặc custom animation:
class MessageTransition extends StatefulWidget {
final Widget child;
final bool animate;
const MessageTransition({required this.child, this.animate = false});
@override
State<MessageTransition> createState() => _MessageTransitionState();
}
class _MessageTransitionState extends State<MessageTransition>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _slide;
late Animation<double> _fade;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_slide = Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_fade = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
if (widget.animate) _controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!widget.animate) return widget.child;
return SlideTransition(
position: _slide,
child: FadeTransition(opacity: _fade, child: widget.child),
);
}
}
Khi nào apply
| Scenario | Animation |
|---|---|
| Send message mới | View Transition (old→new) |
| Chat liên tục (send tiếp) | View Transition (old→new) |
| F5 / load history | Không |
| Stream chunk update | Không |
| Switch session | Không |
Timeline
0ms User click Send
├─ startViewTransition() bắt đầu
└─ Browser chụp screenshot OLD
~16ms Callback chạy (DOM update)
├─ addMessage(user)
├─ setPlaceholderHeight()
└─ scrollIntoView(liveWrapper)
~32ms Browser chụp screenshot NEW
└─ Animate bắt đầu:
OLD: opacity 1→0 (cross-fade out)
NEW: translateY(20vh)→0, opacity 0→1 (slide up + fade in)
400ms Animation done
├─ OLD pseudo-element removed
└─ NEW content visible bình thường
Browser Support
| Browser | Support |
|---|---|
| Chrome 111+ | ✅ |
| Edge 111+ | ✅ |
| Safari 18+ | ✅ |
| Firefox 126+ | ✅ |
Fallback: không animation, DOM update bình thường. Logic scroll/placeholder vẫn hoạt động.
Checklist
- CSS
@keyframes slide-up+::view-transition-old/new(root) - JS:
document.startViewTransition(doUpdate)khi send message - Fallback:
doUpdate()trực tiếp khi API không support - Apply cho mọi send message (bao gồm chat liên tục)
- KHÔNG apply cho F5, stream update, switch session