Drop Your Animation Library: 5 Lines to Make Your SPA Feel Like a Native App
Are you importing Framer Motion just for page transitions?
Writing opacity + transform by hand every time you switch views in your SPA?
In October 2025, the View Transitions API landed in Baseline — zero dependencies, five lines of code, supported in all major browsers except Firefox (for MPA mode).
What It Actually Does
In short: it automatically animates between before and after DOM states.
The browser does three things under the hood:
- Takes a screenshot of the old state
- Runs your callback to update the DOM
- Takes a screenshot of the new state and animates the diff
Everything happens within a single frame. Users just see a smooth transition. Default is crossfade. Add CSS and you can do slides, morphs — anything.
Dropping It Into a Real SPA: 5 Lines
I tested this on a Flask + SPA internal dashboard I maintain. Here's the original view-switching code:
function showView(name) {
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
document.getElementById('view-' + name).style.display = 'block';
updateActiveNav(name);
}
Making it View Transitions-aware:
function showView(name) {
const doSwitch = () => {
document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
document.getElementById('view-' + name).style.display = 'block';
updateActiveNav(name);
};
if (document.startViewTransition) {
document.startViewTransition(doSwitch);
} else {
doSwitch(); // fallback: works normally without animation
}
}
That's it. The overview → family → nodes tab switches now have a soft crossfade. No build step, no new dependency, no bundle size increase.
Customizing the Animation
If the default fade isn't your thing, override it with CSS:
/* Slide left-to-right */
::view-transition-old(root) {
animation: slide-out 0.25s ease-out;
}
::view-transition-new(root) {
animation: slide-in 0.25s ease-out;
}
@keyframes slide-out {
to { transform: translateX(-40px); opacity: 0; }
}
@keyframes slide-in {
from { transform: translateX(40px); opacity: 0; }
}
Element-Level Morphing
The real power move is view-transition-name. Give the same name to elements across two states and the browser automatically interpolates their position and size.
/* Same name on thumbnail and fullscreen view */
.thumbnail { view-transition-name: hero-image; }
.fullscreen { view-transition-name: hero-image; }
Click to expand, click to collapse — native app feel without GSAP.
vs. Framer Motion / GSAP
| View Transitions API | Framer Motion | GSAP | |
|---|---|---|---|
| Bundle Size | 0 KB | ~50 KB | ~25 KB |
| React dependency | None | Required | None |
| MPA support | ✅ | ❌ | ❌ |
| Firefox | ❌ (MPA) / ✅ (SPA) | ✅ | ✅ |
For MPAs (multi-page apps), it's even simpler — just two lines of CSS for full page-transition animation:
@view-transition {
navigation: auto;
}
Gotchas to Avoid
1. Elements animate as images during the transition
The snapshot is a bitmap — so font-size changes or clip-path won't apply during the animation phase.
2. view-transition-name must be unique per page
If multiple elements share the same name, only the last one gets transitioned. For dynamic lists, use view-transition-name: item-${id}.
3. Firefox MPA support is pending
SPA (startViewTransition) works in Firefox 133+. MPA (@view-transition { navigation: auto }) is expected sometime in 2026 — use @supports as a fallback for now.
TL;DR
-
SPA: Wrap your existing DOM updates in
document.startViewTransition() - MPA: 2 lines of CSS for instant page-transition animation
- Graceful degradation: Unsupported browsers just update the DOM normally — nothing breaks
- Cost: 0 KB
Before reaching for a new library, check what the browser already ships for free. That discipline, applied consistently, is what keeps codebases maintainable long-term.
Top comments (0)