|
| 1 | +--- |
| 2 | +title: Cookbook | View Transition API |
| 3 | +contributors: |
| 4 | + - GrandSchtroumpf |
| 5 | +--- |
| 6 | + |
| 7 | +# View Transition API |
| 8 | + |
| 9 | +By default Qwik will start a view transition when SPA navigation. We can run animation either with CSS or WAAPI. |
| 10 | + |
| 11 | +## CSS |
| 12 | + |
| 13 | +```tsx |
| 14 | +export default component$(({ list }) => { |
| 15 | + return ( |
| 16 | + <ul> |
| 17 | + {list.map((item) => ( |
| 18 | + // Create a name per item |
| 19 | + <li key={item.id} class="item" style={{ '--view-transition-name': `_${item.id}_` }}> |
| 20 | + ... |
| 21 | + </li> |
| 22 | + ))} |
| 23 | + </ul> |
| 24 | + ); |
| 25 | +}); |
| 26 | +``` |
| 27 | + |
| 28 | +```css |
| 29 | +.item { |
| 30 | + view-transition-name: var(--view-transition-name); |
| 31 | + /* Alias to target all .item with a view-transition-name */ |
| 32 | + view-transition-class: animated-item; |
| 33 | +} |
| 34 | +/* Animate when item didn't exist in the previous page */ |
| 35 | +::view-transition-new(.animated-item):only-child { |
| 36 | + animation: fade-in 200ms; |
| 37 | +} |
| 38 | +/* Animate when item doesn't exist in the next page */ |
| 39 | +::view-transition-old(.animated-item):only-child { |
| 40 | + animation: fade-out 200ms; |
| 41 | +} |
| 42 | + |
| 43 | +/* If view transition type is supported */ |
| 44 | +@supports selector(html:active-view-transition-type(type)) { |
| 45 | + /* Remove view transition name when view transition is not "qwik-navigation" */ |
| 46 | + html:not(:active-view-transition-type(qwik-navigation)) .items { |
| 47 | + view-transition-name: none; |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +Sometime we need to have some specific logic before the animation start. In this case you can listen to the `qviewTransition` event. |
| 53 | + |
| 54 | +For example if you want to only animate visible element: |
| 55 | + |
| 56 | +```tsx |
| 57 | +export default component$(() => { |
| 58 | + // In this case we need the callback to be sync, else the transition might have already happened |
| 59 | + useOnDocument( |
| 60 | + 'qviewTransition', |
| 61 | + sync$((event: CustomEvent<ViewTransition>) => { |
| 62 | + const transition = event.detail; |
| 63 | + const items = document.querySelectorAll('.item'); |
| 64 | + for (const item of items) { |
| 65 | + if (!item.checkVisibility()) continue; |
| 66 | + item.dataset.hasViewTransition = true; |
| 67 | + } |
| 68 | + }) |
| 69 | + ); |
| 70 | + return ( |
| 71 | + <ul> |
| 72 | + {list.map((item) => ( |
| 73 | + // Create a name per item |
| 74 | + <li key={item.id} class="item" style={{ '--view-transition-name': `_${item.id}_` }}> |
| 75 | + ... |
| 76 | + </li> |
| 77 | + ))} |
| 78 | + </ul> |
| 79 | + ); |
| 80 | +}); |
| 81 | +``` |
| 82 | + |
| 83 | +```css |
| 84 | +.item[data-has-view-transition='true'] { |
| 85 | + view-transition-name: var(--view-transition-name); |
| 86 | + view-transition-class: animated-item; |
| 87 | +} |
| 88 | +::view-transition-new(.animated-item):only-child { |
| 89 | + animation: fade-in 200ms; |
| 90 | +} |
| 91 | +::view-transition-old(.animated-item):only-child { |
| 92 | + animation: fade-out 200ms; |
| 93 | +} |
| 94 | + |
| 95 | +/* If view transition type is supported */ |
| 96 | +@supports selector(html:active-view-transition-type(type)) { |
| 97 | + /* Remove view transition name when view transition is not "qwik-navigation" */ |
| 98 | + html:not(:active-view-transition-type(qwik-navigation)) .items[data-has-view-transition='true'] { |
| 99 | + view-transition-name: none; |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +> **Note**: `ViewTransition` interface is available with Typescript >5.6. |
| 105 | +
|
| 106 | +## WAAPI |
| 107 | + |
| 108 | +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. |
| 109 | + |
| 110 | +In this example we add some delay for each item : |
| 111 | + |
| 112 | +```tsx |
| 113 | +export default component$(() => { |
| 114 | + // Remove default style on the pseudo-element. |
| 115 | + useStyles$(` |
| 116 | + li { |
| 117 | + view-transition-class: items; |
| 118 | + } |
| 119 | + ::view-transition-old(.items) { |
| 120 | + animation: none; |
| 121 | + } |
| 122 | + `); |
| 123 | + useOnDocument( |
| 124 | + 'qviewTransition', |
| 125 | + $(async (event: CustomEvent<ViewTransition>) => { |
| 126 | + // Get visible item's viewTransitionName (should happen before transition is ready) |
| 127 | + const items = document.querySelectorAll<HTMLElement>('.item'); |
| 128 | + const names = Array.from(items) |
| 129 | + .filter((item) => item.checkVisibility()) |
| 130 | + .map((item) => item.style.viewTransitionName); |
| 131 | + |
| 132 | + // Wait for ::view-transition pseudo-element to exist |
| 133 | + const transition = event.detail; |
| 134 | + await transition.ready; |
| 135 | + |
| 136 | + // Animate each leaving item |
| 137 | + for (let i = 0; i < names.length; i++) { |
| 138 | + // Note: we animate the <html> element |
| 139 | + document.documentElement.animate( |
| 140 | + { |
| 141 | + opacity: 0, |
| 142 | + transform: 'scale(0.9)', |
| 143 | + }, |
| 144 | + { |
| 145 | + // Target the pseudo-element inside the <html> element |
| 146 | + pseudoElement: `::view-transition-old(${names[i]})`, |
| 147 | + duration: 200, |
| 148 | + fill: 'forwards', |
| 149 | + delay: i * 50, // Add delay for each pseudo-element |
| 150 | + } |
| 151 | + ); |
| 152 | + } |
| 153 | + }) |
| 154 | + ); |
| 155 | + return ( |
| 156 | + <ul> |
| 157 | + {list.map((item) => ( |
| 158 | + // Create a name per item |
| 159 | + <li key={item.id} class="item" style={{ viewTransitionName: `_${item.id}_` }}> |
| 160 | + ... |
| 161 | + </li> |
| 162 | + ))} |
| 163 | + </ul> |
| 164 | + ); |
| 165 | +}); |
| 166 | +``` |
| 167 | + |
| 168 | +When listening on the `qviewTransition` we know that |
| 169 | + |
| 170 | +> **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. |
| 171 | +
|
| 172 | +## Root transition |
| 173 | + |
| 174 | +By default Qwik disables root view transition inside the `@layer qwik`. If you want to enable it you need to reset it : |
| 175 | + |
| 176 | +```css |
| 177 | +:root { |
| 178 | + view-transition-name: root; |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +or within a `@layer` : |
| 183 | + |
| 184 | +```css |
| 185 | +@layer qwik, reset; |
| 186 | +@layer reset { |
| 187 | + :root { |
| 188 | + view-transition-name: root; |
| 189 | + } |
| 190 | +} |
| 191 | +``` |
0 commit comments