Want to skip the docs? Check out pandamastery.com - the best way to learn Panda CSS

styling
view transition

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:

OptionCSS 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.

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. There's no theme.viewTransitions config registry to register this in. If you've shared a cva or a plain css() helper from a design system before, this works the same way.