Front-end education for the real world. Since 2018.





Start implementing view transitions on your websites today

Cyd Stumpel

Topic: CSS

The View Transition API allows us to animate between two states with relative ease. I say relative ease, but view transitions can get quite complicated fast.

A view transition can be called in two ways; if you add a tiny bit of CSS, a view transition is initiated on every page change, or you can initiate it manually with JavaScript.

Code language
css

@view-transition {
	navigation: auto;
}

SPAs and most frameworks require you to call view transitions for page transitions manually, through JavaScript. If you don’t mess with the browser’s native routing you can use this CSS approach.

Code language
js

if (document.startViewTransition()) {
	document.startViewTransition(() => {
		// If view transitions are supported we call a
		// function encased in a viewTransition
		filterItems()
	})
} else {
	// If view transitions are not supported we still
	// call the filter function
	filterItems()
}

When a view transition is initiated it creates two snapshots; one of the current state, and one the future state. The view transition then compares the position, sizing and rotation of the two snapshots and, finally creates a keyframe animation. It’s pretty much how the FLIP animation technique works, but CSS does all the heavy lifting, even if JavaScript initiates the view transition.

Those snapshots end up in a view transition pseudo element, and by default only a snapshot of the root (HTML) element is created, but you can add more items to the pseudo element by adding view-transition-names to elements.

Anatomy of a view transitionpermalink

Code language
text

::view-transition
	::view-transition-group(root)
		::view-transition-image-pair(root)
			::view-transition-old(root)
			::view-transition-new(root)

Every named view transition gets its own group. Every group has a view-transition-image-pair and a view-transition-old and/or view-transition-new pseudo element.

  • The view transition group is where the custom matrix animation is added to animate to the new state’s position, rotation and size.
  • The view transition image pair is used to isolate the mix-blend-mode animation that’s on the view-transition-old and view-transition-new states by default.
  • The view-transition-old and view-transition-new states represent the old and new state of the named element.

The snapshots are no longer HTML because it’s a non interactive snapshot of the element as it was when it was captured by the initiation of the view transition. Adding CSS to the view transition selector to change font size or colour, for example, or trying to change the content with JavaScript during the view transition doesn’t work.

Debugging view transitionspermalink

You can debug your view transitions with the Animations Drawer in the Chrome Dev Tools, this drawer allows you to slow down animations and even to pause animations, which really gives you some time to inspect what’s going on.

Use CMD + Shift + p in the dev tools and type animations to open up the Animations Drawer. For Windows users, switch CMD with CTRL.

The animations drawer as described above

Unique view transition names and match-elementpermalink

Adding an element to the view transition pseudo element is easy: give it a unique name with view-transition-name.

For Same Document View Transitions you can set the view-transition name to match-element, but if you animate between pages you have to manually add unique view transition names to the elements on both pages.

Setting up your project for view transitionspermalink

View transitions can be used to animate filtering, sorting, add to cart, page transitions, and much more, but when you start doing multiple view transitions triggered by different elements and different user interactions, it’s going to be hard to see the forest through the trees.

View Transition Types are the answer here, you can add types through JavaScript when you call a view transition:

Code language
js

if (document.startViewTransition) {
	document.startViewTransition({
		update: () => filterItems(),
		types: ['filter']
	})
} else {
	filterItems()
}

I recently found out that the first Chrome versions that supported view transitions, do not support View Transition Types which breaks the entire view transition 🥲. Bramus van Damme sent me this try-catch function you could use to catch that. But you could also opt to use data attributes on the HTML tag instead.

The types are added as a pseudo-class :active-view-transition-type(filter) that you can use to encase your specific styles for that interaction. This is very helpful if you want to have different animations on filter and page transition for example.

For specific page transitions between overview and detail pages we can also add a view transition type, but it’s a little more complicated because we currently need to use JavaScript to watch for the pagereveal event and check the from and entry url. Page reveal is called when a user navigates to a new page.

A little bird told we might be able to do this directly from CSS soon!

Code language
js

window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
	  // set a default transition type, you could also leave this empty
    let transitionType = 'normal'
    // check if navigation activation is defined and use it to get
    // from and to url:
    if (navigation?.activation?.from && navigation?.activation?.entry) {
      transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry)
    }
    e.viewTransition.types.add(transitionType)
  }
})

const determineTransitionType = (from, to) => {
  const currentUrl = from?.url ? new URL(from.url) : null
  const targetUrl = new URL(to.url)
  // get paths:
  let currentPath = currentUrl.pathname
  let targetPath = targetUrl.pathname
	const fromType = getPageType(currentPath)
	const toType = getPageType(targetPath)
	return `${fromType}-to-${toType}`
}

const getPageType = (path) => {
	// remove first /
  path = path.replace('/', '')
  // Split path into segments (/blog/view-transitions would be
  // split in 'blog' and 'view-transitions' f.e.)
  const segments = path.split('/')
  const [firstSegment, secondSegment] = segments

  switch (firstSegment) {
    case '':
      return 'home'
    case 'work':
    case 'blog':
      return secondSegment
        ? `${firstSegment}-detail`
        : `${firstSegment}-overview`
    default:
      return 'normal'
  }
}

Going from /work/ to /work/piccalilli would return work-overview-to-work-detail as a view transition type.

Good defaults and best practicespermalink

Code language
css

::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
	animation-duration: 0.6s;
	animation-fill-mode: forwards; /* important if you mix durations */
	animation-timing-function: var(--default-ease);
}

This will give your animations the same base duration, fill mode and ease.

Code language
css

::view-transition-old(.work-item) {
	/* animation: scale-out 0.4s; < 👎 this will override
	   your fill-mode and ease */
	animation-name: scale-out;
	animation-duration: 0.4s;
}

Code language
html

<article class="work-item" style="--vt: work-item-1">
	<!-- [...] -->
</article>

Code language
css

.work-item {
	@media (prefers-reduced-motion: no-preference) {
		view-transition-name: var(--vt);
	}
	view-transition-class: work-item;
}

This approach has several advantages:

  • It’s easier to create unique view transition names, especially if you’re using a CMS or framework, using an id or index value in the name.

  • It keeps your HTML from looking like a framework boilerplate like Tailwind because --vt is nice and short

  • If you have multiple view transition types you can conditionally add the items to the pseudo element easier.

    Code language
    css
    
    html:active-view-transition-type(filter) {
    	.work-item {
    		@media (prefers-reduced-motion: no-preference) {
    			view-transition-name: var(--vt);
    		}
    		view-transition-class: work-item;
    	}
    }
    
    
  • If you want to add all view transition names at once you can use an attribute selector:

    Code language
    css
    
    [style*="--vt"] {
    	@media (prefers-reduced-motion: no-preference) {
    		view-transition-name: var(--vt);
    	}
    }
    
    

The best thing to do is encase all your view-transition-name declarations in a prefers reduced motion media query set to no-preference which will default all view-transitions back to just cross-fading the root element.

Code language
css

@media (prefers-reduced-motion: no-preference) {
	view-transition-name: var(--vt);
}

Taking full advantage of the View Transition APIpermalink

As I mentioned earlier, view transition groups consist of a view-transition-image-pair which has either view-transition-old, view-transition-new or both. Whether there is an old and/or a new state depends on if the named view-transition element exists in the old state and the new state.

In practice this means that for animations that change the order, like sorting, items will have an old and a new state, because they exist in the old state and the new state.

During the view transition, by default, the view-transition-old pseudo element crossfades into the view-transition-new state, represented by the red and green rectangle respectively.

If you filter items out, those items will only have a view-transition-old state because they do not exist in the new state:

In the same way, if filters add items back in, they will only have a view-transition-new state because they did not exist in the old state:

With CSS we can check if view-transition-old or view-transition-new is an only child using the :only-child pseudo-class, allowing us to create in-and-out animations for them.

Code language
css

::view-transition-old(work-item):only-child {
	animation-name: animate-out;
	animation-duration: 0.3s;
}

::view-transition-new(work-item):only-child {
	animation-name: animate-in;
	animation-duration: 0.3s;
}

Try out this demo. When you apply sorting, the in-and-out animations are not triggered but when you filter they are!

See the Pen View transitions sorting and filtering by piccalilli (@piccalilli) on CodePen.

This is pretty cool for filtering, but also when you want to transition from an overview to a detail page or back like I did for this website I created for my CSS Day Talk. In this talk I also mentioned we’re currently not able to use :has to check if a group has both view-transition-old and -new elements, because this would allow us to set a higher z-index on items that have both, and push them in front of the other elements. But complaining on a stage helps because that’s now been added as an issue!

Browser compatibility

With the release of Firefox 144, the View Transition API has now been implemented in all the ‘big browsers’ 🥳.

Unfortunately, though, Firefox currently only supports same-document view transitions, not transitions between pages (also known cross-document view transitions).

If you want to follow along when they will support that, you can check MDN or this handy CodePen that the Chrome team created.

See the Pen View Transitions Feature Explorer by piccalilli (@piccalilli) on CodePen.

Enjoyed this article? You can support us by leaving a tip via Open Collective


Newsletter