View Transitions
Style View Transitions API animations with a shared class. You still set a unique name per element.
How it works
The View Transitions API needs two things:
- A unique
view-transition-nameon each element (duplicates break the transition) - Shared animation CSS for
::view-transition-old(*)/::view-transition-new(*)and friends
Panda can't own the name. Those have to stay unique, so it can't extract or dedupe them. Instead,
viewTransition() returns a class built on
view-transition-class (opens in a new tab).
import { viewTransition } from 'styled-system/css'
const slide = viewTransition({
group: {
animationDuration: '0.4s',
animationTimingFunction: 'ease-in-out'
},
old: { opacity: 0 },
new: { opacity: 1 }
})
// → "vt_xxx".vt_xxx {
view-transition-class: vt_xxx;
}
::view-transition-group(.vt_xxx) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
::view-transition-old(.vt_xxx) {
opacity: 0;
}
::view-transition-new(.vt_xxx) {
opacity: 1;
}Pseudo-elements
| Option | Selector |
|---|---|
group | ::view-transition-group(.vt_*) |
imagePair | ::view-transition-image-pair(.vt_*) |
old | ::view-transition-old(.vt_*) |
new | ::view-transition-new(.vt_*) |
Each option takes a style object, same shape as css(). Set animationName to a theme keyframe or any animation name.
Framework usage
viewTransition() only returns the class. How you set view-transition-name depends on the framework.
React
Needs a React build that exports <ViewTransition> (Canary / Experimental), or Next.js with
experimental.viewTransition: true.
import { ViewTransition } from 'react'
import { viewTransition } from 'styled-system/css'
const slide = viewTransition({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})
<ViewTransition name="hero" share={slide}>
<img src="/hero.jpg" alt="Hero" />
</ViewTransition>Pass the class into enter, exit, share, update, or default. Use name when elements should morph into each
other.
Astro
Astro owns the name via transition:name. Put the class on the element:
---
import { viewTransition } from '../styled-system/css'
const slide = viewTransition({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})
---
<img class={slide} transition:name="hero" src="/hero.jpg" alt="Hero" />Enable client routing with <ClientRouter /> from astro:transitions, or the browser never starts a transition.
Other frameworks
Set both the class and the name yourself:
<img
className={slide}
style={{ viewTransitionName: 'hero' }}
src="/hero.jpg"
alt="Hero"
/>Named transitions
Register reusable bags in theme.viewTransitions, then call them by name:
panda.config.ts
import { defineConfig, defineViewTransitions } from '@pandacss/dev'
export default defineConfig({
theme: {
extend: {
viewTransitions: defineViewTransitions({
slide: {
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
}
})
}
}
})import { ViewTransition } from 'react'
import { viewTransition } from 'styled-system/css'
<ViewTransition name="hero" share={viewTransition('slide')}>
<img src="/hero.jpg" alt="Hero" />
</ViewTransition>viewTransition('slide') returns "vt_slide". A static name inlines at build time and drops the import. Unused names
stay out of the CSS.
Design systems
Export a bag the same way you'd export a shared cva:
export const slide = viewTransition({
old: { opacity: 0 },
new: { opacity: 1 }
})panda lib puts it in build info. Consuming apps get the CSS without re-scanning your source, and tree-shaking follows
imports like recipes. Or register the same bags in a preset's theme.viewTransitions and let apps call
viewTransition('slide').