viewTransition()
Style shared View Transitions API animations with a hashed bag class. You still set unique view-transition-name values at runtime.
The View Transitions API styles its animation with ::view-transition-old(name) and ::view-transition-new(name),
matched against a view-transition-name you set on the element. That name has to be unique per element. Two elements
with the same name break the transition.
Panda extracts and shares styles across every place you call a style function. Those two rules don't mix: if the CSS
were keyed on view-transition-name, Panda couldn't dedupe it, since the name is different every time. So Panda
doesn't touch view-transition-name at all. Instead, viewTransition() gives you a class, generated from a real
CSS property built for exactly this, view-transition-class. Panda can extract and share a class like any other
atomic style. You still set the unique name yourself, however your framework wants it.
Import viewTransition from styled-system/css.
Basic example
import { viewTransition } from 'styled-system/css'
const slide = viewTransition({
group: {
animationDuration: '0.4s',
animationTimingFunction: 'ease-in-out'
},
imagePair: { isolation: 'isolate' },
old: { opacity: 0 },
new: { opacity: 1 }
})
// → "vt_xxx"
slide is a class name, "vt_xxx". Panda emits the CSS behind it:
@layer utilities {
.vt_xxx {
view-transition-class: vt_xxx;
}
::view-transition-group(.vt_xxx) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
::view-transition-image-pair(.vt_xxx) {
isolation: isolate;
}
::view-transition-old(.vt_xxx) {
opacity: 0;
}
::view-transition-new(.vt_xxx) {
opacity: 1;
}
}
Call viewTransition() again somewhere else with the exact same options and you get the exact same class back, no
duplicate CSS. That's the whole point: one shared bag class, however many elements use it.
Wiring it into your framework
viewTransition() only gets you the class. Every framework has its own way of setting view-transition-name, so
that part is still on you.
React
Pass the class into React's View Transition Class props, enter, exit, share, update, default. Reserve
name for elements that need a shared-element transition:
import { ViewTransition } from 'react'
import { viewTransition } from 'styled-system/css'
const slide = viewTransition({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})
export function HeroImage() {
return (
<ViewTransition name="hero" share={slide}>
<img src="/hero.jpg" alt="Hero" />
</ViewTransition>
)
}
<ViewTransition> needs a React build that exports it, Canary or Experimental as of this writing. Next.js can opt in
with experimental.viewTransition: true.
Astro
Astro owns the name through transition:name. Put the bag class on the element next to it:
---
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 in your layout, or the browser never starts a
transition to animate.
Solid or Nuxt
Neither framework wraps this for you, so you set both the class and the name yourself:
import { viewTransition } from 'styled-system/css'
const slide = viewTransition({
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
})
<img
class={slide}
style={{ viewTransitionName: 'hero' }}
src="/hero.jpg"
alt="Hero"
/>
Slots
Four options, each one a slot in the View Transitions pseudo-element tree:
| Option | CSS selector |
|---|---|
group | ::view-transition-group(.vt_*) |
imagePair | ::view-transition-image-pair(.vt_*) |
old | ::view-transition-old(.vt_*) |
new | ::view-transition-new(.vt_*) |
Each one takes a style object, same shape as css(). For animations, set animationName to a theme keyframe or a
name you defined yourself.
Share from a preset or config
Put named bags in theme.viewTransitions so the team calls viewTransition('slide') instead of copying the object.
view-transitions.ts
import { defineViewTransitions } from '@pandacss/dev'
export const viewTransitions = defineViewTransitions({
slide: {
group: { animationDuration: '0.4s' },
old: { opacity: 0 },
new: { opacity: 1 }
}
})panda.config.ts
import { defineConfig } from '@pandacss/dev'
import { viewTransitions } from './view-transitions'
export default defineConfig({
theme: {
extend: {
viewTransitions
}
}
})import { viewTransition } from 'styled-system/css'
viewTransition('slide')
// → "vt_slide"
A static name becomes a string at build time. The import is dropped. Unused names stay out of the CSS.
Use it the same way as the object form: React share / enter / exit, or the class next to Astro's transition:name.
<ViewTransition name="hero" share={viewTransition('slide')}>
<img src="/hero.jpg" alt="Hero" />
</ViewTransition>
You can still write viewTransition({ old, new }) for a one-off. That keeps the hashed vt_* class.
Shipping a shared transition from a design system
Export the bag as a constant from your library source, the same way you'd export a shared cva:
export const slide = viewTransition({
old: { opacity: 0 },
new: { opacity: 1 }
})
panda lib serializes it into build info, so apps consuming your library get the CSS without re-scanning your
source. It tree-shakes the same way recipes do, only the bags an app actually imports ship.
A preset can also register the same bags in theme.viewTransitions and let apps call viewTransition('slide').