Skip to content

Commit 2ba1b34

Browse files
wait for transition to be ready
1 parent bd7f514 commit 2ba1b34

File tree

3 files changed

+215
-12
lines changed

3 files changed

+215
-12
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
```

packages/qwik-router/src/runtime/src/qwik-router-component.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -645,8 +645,26 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
645645
saveScrollHistory(scrollState);
646646
}
647647

648-
clientNavigate(window, navType, prevUrl, trackUrl, replaceState);
649-
_waitUntilRendered(elm as Element).then(() => {
648+
const navigate = () => {
649+
clientNavigate(window, navType, prevUrl, trackUrl, replaceState);
650+
return _waitUntilRendered(elm as Element);
651+
};
652+
653+
const _waitNextPage = () => {
654+
if (isServer || props.viewTransition === false) {
655+
return navigate();
656+
} else {
657+
const viewTransition = startViewTransition({
658+
update: navigate,
659+
types: ['qwik-navigation'],
660+
});
661+
if (!viewTransition) {
662+
return Promise.resolve();
663+
}
664+
return viewTransition.ready;
665+
}
666+
};
667+
_waitNextPage().then(() => {
650668
const container = _getQContainerElement(elm as _ElementVNode)!;
651669
container.setAttribute('q:route', routeName);
652670
const scrollState = currentScrollState(scroller);
@@ -665,14 +683,8 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
665683

666684
if (isServer) {
667685
return run();
668-
}
669-
if (props.viewTransition === false) {
670-
run();
671686
} else {
672-
startViewTransition({
673-
update: run,
674-
types: ['qwik-router-spa'],
675-
});
687+
run();
676688
}
677689
});
678690

starters/e2e/qwikrouter/nav.e2e.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ test.describe("actions", () => {
105105

106106
await scrollDetector1;
107107
await expect(page.locator("h1")).toHaveText("Page Short");
108-
expect(page).toHaveURL(
108+
await expect(page).toHaveURL(
109109
"/qwikrouter-test/scroll-restoration/page-short/",
110110
);
111111
expect(await getWindowScrollXY(page)).toStrictEqual([0, 0]);
@@ -124,7 +124,7 @@ test.describe("actions", () => {
124124

125125
await scrollDetector2;
126126
await expect(page.locator("h1")).toHaveText("Page Long");
127-
expect(page).toHaveURL(
127+
await expect(page).toHaveURL(
128128
"/qwikrouter-test/scroll-restoration/page-long/",
129129
);
130130
expect(await getWindowScrollXY(page)).toStrictEqual([
@@ -137,7 +137,7 @@ test.describe("actions", () => {
137137

138138
await scrollDetector3;
139139
await expect(page.locator("h1")).toHaveText("Page Short");
140-
expect(page).toHaveURL(
140+
await expect(page).toHaveURL(
141141
"/qwikrouter-test/scroll-restoration/page-short/",
142142
);
143143
expect(await getWindowScrollXY(page)).toStrictEqual([

0 commit comments

Comments
 (0)