Creates smooth animated transitions between views using the native View Transitions API. Use when building page transitions, state changes, or morphing elements between different DOM states in SPAs and MPAs.
Adds smooth animated transitions between views using the native View Transitions API. Use when building page transitions, state changes, or morphing elements between different DOM states in SPAs and MPAs.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Native browser API for smooth animated transitions between different views. Works in SPAs (JavaScript-triggered) and MPAs (navigation-based).
// Check for support
if (!document.startViewTransition) {
updateDOM();
return;
}
// Trigger transition
document.startViewTransition(() => {
updateDOM();
});
The API:
No JavaScript needed. Opt in with CSS on both pages:
@view-transition {
navigation: auto;
}
Transitions happen automatically on same-origin navigations.
/* Customize the default cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
}
/* Fade out old view */
::view-transition-old(root) {
animation: fade-out 0.3s ease-out both;
}
/* Fade in new view */
::view-transition-new(root) {
animation: fade-in 0.3s ease-in both;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
Give specific elements their own transition behavior.
/* Assign a name to an element */
.header {
view-transition-name: header;
}
.main-image {
view-transition-name: main-image;
}
/* Names must be unique! */
/* Style transitions for named elements */
::view-transition-old(main-image),
::view-transition-new(main-image) {
animation-duration: 0.4s;
}
/* Keep header fixed during transition */
::view-transition-group(header) {
animation: none;
}
When a view transition runs, this pseudo-element tree is created:
::view-transition
└─ ::view-transition-group(name)
└─ ::view-transition-image-pair(name)
├─ ::view-transition-old(name)
└─ ::view-transition-new(name)
| Pseudo | Purpose |
|---|---|
::view-transition | Overlay containing all transitions |
::view-transition-group(name) | Container for size/position animation |
::view-transition-image-pair(name) | Contains old and new snapshots |
::view-transition-old(name) | Screenshot of old state |
::view-transition-new(name) | Live representation of new state |
Categorize transitions for different styling:
document.startViewTransition({
update: () => updateDOM(),
types: ['slide-left'] // Add type identifiers
});
/* Style based on transition type */
html:active-view-transition-type(slide-left) {
&::view-transition-old(root) {
animation: slide-out-left 0.3s ease;
}
&::view-transition-new(root) {
animation: slide-in-right 0.3s ease;
}
}
@keyframes slide-out-left {
to { transform: translateX(-100%); }
}
@keyframes slide-in-right {
from { transform: translateX(100%); }
}
const transition = document.startViewTransition(() => {
// Update DOM here
});
// Promises for timing
transition.ready.then(() => {
// Pseudo-elements created, animation about to start
});
transition.updateCallbackDone.then(() => {
// DOM update callback finished
});
transition.finished.then(() => {
// Animation complete
});
// Skip the animation
transition.skipTransition();
const transition = document.startViewTransition(() => updateDOM());
transition.ready.then(() => {
// Use Web Animations API on pseudo-elements
document.documentElement.animate(
[
{ clipPath: 'circle(0% at 50% 50%)' },
{ clipPath: 'circle(100% at 50% 50%)' }
],
{
duration: 500,
easing: 'ease-out',
pseudoElement: '::view-transition-new(root)'
}
);
});
function useViewTransition() {
const startTransition = (callback) => {
if (!document.startViewTransition) {
callback();
return;
}
document.startViewTransition(callback);
};
return { startTransition };
}
// Usage
function Component() {
const { startTransition } = useViewTransition();
const [page, setPage] = useState('home');
const navigate = (newPage) => {
startTransition(() => setPage(newPage));
};
return (
<div>
<nav>
<button onClick={() => navigate('home')}>Home</button>
<button onClick={() => navigate('about')}>About</button>
</nav>
{page === 'home' && <HomePage />}
{page === 'about' && <AboutPage />}
</div>
);
}
import { useNavigate } from 'react-router-dom';
function NavLink({ to, children }) {
const navigate = useNavigate();
const handleClick = (e) => {
e.preventDefault();
if (!document.startViewTransition) {
navigate(to);
return;
}
document.startViewTransition(() => {
navigate(to);
});
};
return <a href={to} onClick={handleClick}>{children}</a>;
}
import { unstable_ViewTransition as ViewTransition } from 'react';
function App() {
const [page, setPage] = useState('home');
return (
<ViewTransition>
{page === 'home' ? <Home /> : <About />}
</ViewTransition>
);
}
/* List page */
.thumbnail {
view-transition-name: hero-image;
}
/* Detail page */
.hero-image {
view-transition-name: hero-image;
}
/* The image will smoothly morph between pages */
::view-transition-group(hero-image) {
animation-duration: 0.4s;
}
.header {
view-transition-name: header;
}
/* Keep header in place */
::view-transition-old(header),
::view-transition-new(header) {
animation: none;
mix-blend-mode: normal;
}
@keyframes slide-from-right {
from { transform: translateX(100%); }
}
@keyframes slide-to-left {
to { transform: translateX(-100%); }
}
/* Forward navigation */
html:active-view-transition-type(forward) {
&::view-transition-old(root) {
animation: slide-to-left 0.3s ease forwards;
}
&::view-transition-new(root) {
animation: slide-from-right 0.3s ease forwards;
}
}
/* Back navigation */
html:active-view-transition-type(back) {
&::view-transition-old(root) {
animation: slide-from-right 0.3s ease reverse forwards;
}
&::view-transition-new(root) {
animation: slide-to-left 0.3s ease reverse forwards;
}
}
function circleReveal(x, y) {
const transition = document.startViewTransition(() => updateContent());
transition.ready.then(() => {
const maxRadius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
);
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${maxRadius}px at ${x}px ${y}px)`
]
},
{
duration: 500,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)'
}
);
});
}
// Trigger from click position
button.addEventListener('click', (e) => {
circleReveal(e.clientX, e.clientY);
});
/* In grid */
.card {
view-transition-name: var(--card-id);
}
/* When expanded */
.card-detail {
view-transition-name: var(--card-id);
}
function Card({ id, onClick }) {
return (
<div
className="card"
style={{ '--card-id': `card-${id}` }}
onClick={onClick}
/>
);
}
/* Add to both pages */
@view-transition {
navigation: auto;
}
<!-- page1.html -->
<html class="page-home">
<!-- page2.html -->
<html class="page-about">
/* Styles apply based on destination */
.page-about::view-transition-old(root) {
animation: slide-out-left 0.3s;
}
Use the new match-element value (Chrome 126+):
/* Automatically generate view-transition-name based on element identity */
.product-card {
view-transition-name: match-element;
}
Or manually assign matching names:
/* page1.html */
.product-thumbnail[data-id="123"] {
view-transition-name: product-123;
}
/* page2.html */
.product-hero[data-id="123"] {
view-transition-name: product-123;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
View transitions are purely visual; screen readers see the DOM update immediately.
Chrome DevTools:
::view-transition-* elementsAdd slow-mo for development:
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 3s !important;
}
view-transition-name sparingly - Too many can hurt performanceasync function navigate(callback) {
// Feature detection
if (!document.startViewTransition) {
callback();
return;
}
try {
await document.startViewTransition(callback).finished;
} catch (e) {
// Transition was skipped or errored
// DOM is still updated, just not animated
}
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.