Chuyển tới nội dung chính

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

ScenarioAnimation
Send message mớiView Transition (old→new)
Chat liên tục (send tiếp)View Transition (old→new)
F5 / load historyKhông
Stream chunk updateKhông
Switch sessionKhô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

BrowserSupport
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