Skip to content

Commit 73e8fcc

Browse files
feat: View Transition hook (#7237)
* feat: dispatch q:viewTransition when started * add changeset * add documentation * replace q:viewTransition for qviewTransition * move QwikTransitionEvent up * doc: fix example * doc: improve examples * doc: expose new page * doc: add indication about ViewTransition interface * doc: fix doc typo and add explaination
1 parent 96b533a commit 73e8fcc

File tree

8 files changed

+149
-3
lines changed

8 files changed

+149
-3
lines changed

.changeset/plenty-books-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik': patch
3+
---
4+
5+
Emit an CustomEvent `qviewTransition` when view transition starts.

packages/docs/src/routes/docs/cookbook/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ Examples:
3232
- [Synchronous Events with State](./sync-events/)
3333
- [Theme Management](./theme-management/)
3434
- [Drag & Drop](./drag&drop/)
35+
- [View Transition](./view-transition/)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: Cookbook | View Transition API
3+
contributors:
4+
- GrandSchtroumpf
5+
---
6+
7+
# View Transition API
8+
By default Qwik will start a view transition when SPA navigation. We can run animation either with CSS or WAAPI.
9+
10+
## CSS
11+
```tsx
12+
export default component$(({ list }) => {
13+
return (
14+
<ul>
15+
{list.map((item) => (
16+
// Create a name per item
17+
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
18+
))}
19+
</ul>
20+
)
21+
})
22+
```
23+
24+
```css
25+
.item {
26+
/* Alias to target all .item with a view-transition-name */
27+
view-transition-class: animated-item;
28+
}
29+
/* Animate when item didn't exist in the previous page */
30+
::view-transition-new(.animated-item):only-child {
31+
animation: fade-in 200ms;
32+
}
33+
/* Animate when item doesn't exist in the next page */
34+
::view-transition-old(.animated-item):only-child {
35+
animation: fade-out 200ms;
36+
}
37+
```
38+
39+
Sometime we need to have some specific logic before the animation start. In this case you can listen to the `qviewTransition` event.
40+
41+
For example if you want to only animate visible element:
42+
```tsx
43+
export default component$(() => {
44+
// In this case we need the callback to be sync, else the transition might have already happened
45+
useOnDocument('qviewTransition', sync$((event: CustomEvent<ViewTransition>) => {
46+
const transition = event.detail;
47+
const items = document.querySelectorAll('.item');
48+
for (const item of items) {
49+
if (!item.checkVisibility()) continue;
50+
item.dataset.hasViewTransition = true;
51+
}
52+
}))
53+
return (
54+
<ul>
55+
{list.map((item) => (
56+
// Create a name per item
57+
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
58+
))}
59+
</ul>
60+
)
61+
})
62+
```
63+
64+
```css
65+
.item[data-has-view-transition="true"] {
66+
view-transition-class: animated-item;
67+
}
68+
::view-transition-new(.animated-item):only-child {
69+
animation: fade-in 200ms;
70+
}
71+
::view-transition-old(.animated-item):only-child {
72+
animation: fade-out 200ms;
73+
}
74+
```
75+
76+
> **Note**: `ViewTransition` interface is available with Typescript >5.6.
77+
78+
## WAAPI
79+
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.
80+
81+
In this example we add some delay for each item :
82+
```tsx
83+
export default component$(() => {
84+
// Remove default style on the pseudo-element.
85+
useStyles$(`
86+
li {
87+
view-transition-class: items;
88+
}
89+
::view-transition-old(.items) {
90+
animation: none;
91+
}
92+
`);
93+
useOnDocument('qviewTransition', $(async (event: CustomEvent<ViewTransition>) => {
94+
// Get visible item's viewTransitionName (should happen before transition is ready)
95+
const items = document.querySelectorAll<HTMLElement>('.item');
96+
const names = Array.from(items)
97+
.filter((item) => item.checkVisibility())
98+
.map((item) => item.style.viewTransitionName);
99+
100+
// Wait for ::view-transition pseudo-element to exist
101+
const transition = event.detail;
102+
await transition.ready;
103+
104+
// Animate each leaving item
105+
for (let i = 0; i < names.length; i++) {
106+
// Note: we animate the <html> element
107+
document.documentElement.animate({
108+
opacity: 0,
109+
transform: 'scale(0.9)'
110+
}, {
111+
// Target the pseudo-element inside the <html> element
112+
pseudoElement: `::view-transition-old(${names[i]})`,
113+
duration: 200,
114+
fill: "forwards",
115+
delay: i * 50, // Add delay for each pseudo-element
116+
})
117+
}
118+
}))
119+
return (
120+
<ul>
121+
{list.map((item) => (
122+
// Create a name per item
123+
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
124+
))}
125+
</ul>
126+
)
127+
})
128+
```
129+
130+
> **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.

packages/docs/src/routes/docs/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
- [Sync events w state](/docs/cookbook/sync-events/index.mdx)
5353
- [Theme Management](/docs/cookbook/theme-management/index.mdx)
5454
- [Drag & Drop](/docs/cookbook/drag&drop/index.mdx)
55+
- [View Transition](/docs/cookbook/view-transition/index.mdx)
5556

5657
## Integrations
5758

packages/qwik/src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export type {
139139
QwikVisibleEvent,
140140
QwikIdleEvent,
141141
QwikInitEvent,
142+
QwikTransitionEvent,
142143
// old
143144
NativeAnimationEvent,
144145
NativeClipboardEvent,
@@ -166,7 +167,6 @@ export type {
166167
QwikTouchEvent,
167168
QwikUIEvent,
168169
QwikWheelEvent,
169-
QwikTransitionEvent,
170170
} from './render/jsx/types/jsx-qwik-events';
171171

172172
//////////////////////////////////////////////////////////////////////////////////////////

packages/qwik/src/core/render/dom/visitor.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,10 +1172,15 @@ export const executeContextWithScrollAndTransition = async (ctx: RenderStaticCon
11721172
if (document.__q_view_transition__) {
11731173
document.__q_view_transition__ = undefined;
11741174
if (document.startViewTransition) {
1175-
await document.startViewTransition(() => {
1175+
const transition = document.startViewTransition(() => {
11761176
executeDOMRender(ctx);
11771177
restoreScroll();
1178-
}).finished;
1178+
});
1179+
const event = new CustomEvent('qviewTransition', {
1180+
detail: transition,
1181+
});
1182+
document.dispatchEvent(event);
1183+
await transition.finished;
11791184
return;
11801185
}
11811186
}

packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
QwikIdleEvent,
66
QwikInitEvent,
77
QwikSymbolEvent,
8+
QwikViewTransitionEvent,
89
QwikVisibleEvent,
910
} from './jsx-qwik-events';
1011

@@ -113,6 +114,7 @@ type AllEventMapRaw = HTMLElementEventMap &
113114
qinit: QwikInitEvent;
114115
qsymbol: QwikSymbolEvent;
115116
qvisible: QwikVisibleEvent;
117+
qviewTransition: QwikViewTransitionEvent;
116118
};
117119

118120
/** This corrects the TS definition for ToggleEvent @public */

packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type QwikSymbolEvent = CustomEvent<{ symbol: string; element: Element; re
88
export type QwikInitEvent = CustomEvent<{}>;
99
/** Emitted by qwik-loader on document when the document first becomes idle @public */
1010
export type QwikIdleEvent = CustomEvent<{}>;
11+
/** Emitted by qwik-core on document when the a view transition start @public */
12+
export type QwikViewTransitionEvent = CustomEvent<ViewTransition>;
1113

1214
// Utility types for supporting autocompletion in union types
1315

0 commit comments

Comments
 (0)