Skip to content

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

Advanced

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:

  1. A unique view-transition-name on each element (duplicates break the transition)
  2. 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

OptionSelector
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').

Edit this page on GitHubView as markdown
Last updated on