---
title: Cookbook | View Transition API
contributors:
  - GrandSchtroumpf
---

import { Note } from '~/components/note/note';

# View Transition API

View transitions on SPA navigation are opt-in. Enable them by passing `viewTransition` to `QwikRouterProvider`, then run animation with either CSS or WAAPI.

```tsx
// src/root.tsx
export default component$(() => {
  return (
    <QwikRouterProvider viewTransition={true}>{/* ... */}</QwikRouterProvider>
  );
});
```

## CSS

```tsx
export default component$(({ list }) => {
  return (
    <ul>
      {list.map((item) => (
        // Create a name per item
        <li key={item.id} class="item" style={{ '--view-transition-name': `_${item.id}_` }}>
          ...
        </li>
      ))}
    </ul>
  );
});
```

```css
.item {
  view-transition-name: var(--view-transition-name);
  /* Alias to target all .item with a view-transition-name */
  view-transition-class: animated-item;
}
/* Animate when item didn't exist in the previous page */
::view-transition-new(.animated-item):only-child {
  animation: fade-in 200ms;
}
/* Animate when item doesn't exist in the next page */
::view-transition-old(.animated-item):only-child {
  animation: fade-out 200ms;
}

/* If view transition type is supported */
@supports selector(html:active-view-transition-type(type)) {
  /* Remove view transition name when view transition is not "qwik-navigation" */
  html:not(:active-view-transition-type(qwik-navigation)) .items {
    view-transition-name: none;
  }
}
```

Qwik overrides the user agent default `view-transition-name: root` on `:root` and sets `view-transition-name: none`.

If you want to animate the whole page during SPA navigation, you need to explicitly give the `<html>` element a view transition name.

```css
/* global.css */
html.transition {
  view-transition-name: root;
}
```

Then add the `transition` class to the `<html>` element, so the browser can use `root` as the view transition name for the whole page.

```typescript
/* entry.ssr.tsx */
containerAttributes: {
  class: "transition",
}
```

Alternatively, you can also override the default rule globally by using `! important`

```css
:root {
  view-transition-name: root !important;
}
```

Sometime we need to have some specific logic before the animation start. In this case you can listen to the `qviewTransition` event.

For example if you want to only animate visible element:

```tsx
export default component$(() => {
  // In this case we need the callback to be sync, else the transition might have already happened
  useOnDocument(
    'qviewtransition',
    sync$((event: CustomEvent<ViewTransition>) => {
      const transition = event.detail;
      const items = document.querySelectorAll('.item');
      for (const item of items) {
        if (!item.checkVisibility()) continue;
        item.dataset.hasViewTransition = true;
      }
    })
  );
  return (
    <ul>
      {list.map((item) => (
        // Create a name per item
        <li key={item.id} class="item" style={{ '--view-transition-name': `_${item.id}_` }}>
          ...
        </li>
      ))}
    </ul>
  );
});
```

```css
.item[data-has-view-transition='true'] {
  view-transition-name: var(--view-transition-name);
  view-transition-class: animated-item;
}
::view-transition-new(.animated-item):only-child {
  animation: fade-in 200ms;
}
::view-transition-old(.animated-item):only-child {
  animation: fade-out 200ms;
}

/* If view transition type is supported */
@supports selector(html:active-view-transition-type(type)) {
  /* Remove view transition name when view transition is not "qwik-navigation" */
  html:not(:active-view-transition-type(qwik-navigation)) .items[data-has-view-transition='true'] {
    view-transition-name: none;
  }
}
```

<Note>`ViewTransition` interface is available with Typescript >5.6.</Note>

## WAAPI

With Web Animation API you can get more precise, but for that we need to wait for the ::view-transition pseudo-element to exist in the DOM. To achieve that you can wait the `transition.ready` promise.

In this example we add some delay for each item :

```tsx
export default component$(() => {
  // Remove default style on the pseudo-element.
  useStyles$(`
    li {
      view-transition-class: items;
    }
    ::view-transition-old(.items) {
      animation: none;
    }
  `);
  useOnDocument(
    'qviewtransition',
    $(async (event: CustomEvent<ViewTransition>) => {
      // Get visible item's viewTransitionName (should happen before transition is ready)
      const items = document.querySelectorAll<HTMLElement>('.item');
      const names = Array.from(items)
        .filter((item) => item.checkVisibility())
        .map((item) => item.style.viewTransitionName);

      // Wait for ::view-transition pseudo-element to exist
      const transition = event.detail;
      await transition.ready;

      // Animate each leaving item
      for (let i = 0; i < names.length; i++) {
        // Note: we animate the <html> element
        document.documentElement.animate(
          {
            opacity: 0,
            transform: 'scale(0.9)',
          },
          {
            // Target the pseudo-element inside the <html> element
            pseudoElement: `::view-transition-old(${names[i]})`,
            duration: 200,
            fill: 'forwards',
            delay: i * 50, // Add delay for each pseudo-element
          }
        );
      }
    })
  );
  return (
    <ul>
      {list.map((item) => (
        // Create a name per item
        <li key={item.id} class="item" style={{ viewTransitionName: `_${item.id}_` }}>
          ...
        </li>
      ))}
    </ul>
  );
});
```

When listening on the `qviewtransition` we know that

<Note>For it to work correctly, we need to **remove the default view transition** animation else it happens on top of the `.animate()`. I'm using `view-transition-class` which is only working with Chrome right now.</Note>

## Root transition

By default Qwik disables root view transition inside the `@layer qwik`. If you want to enable it you need to reset it :

```css
:root {
  view-transition-name: root;
}
```

or within a `@layer` :

```css
@layer qwik, reset;
@layer reset {
  :root {
    view-transition-name: root;
  }
}
```
