Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add startViewTransition support #10916

Merged
merged 43 commits into from Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0d5dd29
POC support for startViewTransition
brophdawg11 Mar 9, 2023
069b77f
Add example using startViwTransition
brophdawg11 Jul 14, 2023
9d6f330
Update dep
brophdawg11 Aug 22, 2023
cbd6aad
Update approach and add future flags
brophdawg11 Aug 29, 2023
b774db3
Rename future flag
brophdawg11 Aug 29, 2023
dbac4b1
Updates
brophdawg11 Aug 30, 2023
560a894
Switch to useViewTransition hook
brophdawg11 Sep 19, 2023
d4087e9
Add more updates - proxy through Link
brophdawg11 Sep 20, 2023
c383e8f
Add unstable prefix
brophdawg11 Sep 20, 2023
48303d0
Revert "Add unstable prefix"
brophdawg11 Sep 20, 2023
449d51e
Remove viewTransition API from router.navigate
brophdawg11 Sep 20, 2023
c1dee37
Rename useViewTransitions
brophdawg11 Sep 20, 2023
f516ded
Allow return false
brophdawg11 Sep 20, 2023
f800892
POC
brophdawg11 Sep 22, 2023
adf0b78
Get POP navigations working
brophdawg11 Sep 25, 2023
d8bafcd
Lift viewTransition function execution up to router
brophdawg11 Sep 25, 2023
18cc33b
Handle forward pops
brophdawg11 Sep 25, 2023
27f65c1
Updates
brophdawg11 Sep 26, 2023
9fcf8df
Update approch to use prop and hook together
brophdawg11 Sep 28, 2023
7d6a724
Remove currentLocation from ViewTransitionContext
brophdawg11 Sep 28, 2023
5768b76
Fix image freeze on detail->home
brophdawg11 Sep 28, 2023
e43453e
Remove defer value param
brophdawg11 Sep 28, 2023
f456698
Remove usage of fallbackOnDeferRevalidation
brophdawg11 Sep 28, 2023
2359232
Remove fallbackOnDeferRevalidation flag
brophdawg11 Sep 28, 2023
360206f
Remove debug logs
brophdawg11 Sep 28, 2023
15961e1
Clean ups
brophdawg11 Sep 28, 2023
2f716d3
Revert "Remove debug logs"
brophdawg11 Sep 28, 2023
dda9693
Add interruption handling
brophdawg11 Sep 29, 2023
7263d4a
Remove debug logs
brophdawg11 Sep 29, 2023
098efb7
Remove unused flushSync
brophdawg11 Sep 29, 2023
b9f0c8a
Persist applied transitions to sessionStorage
brophdawg11 Sep 29, 2023
3912731
Add docs
brophdawg11 Oct 2, 2023
4a8a492
Handle basename and PUSH navs that reverse a transition
brophdawg11 Oct 4, 2023
7383054
Udpate docs with example
brophdawg11 Oct 4, 2023
d5e1a89
Udpate docs with example
brophdawg11 Oct 4, 2023
35c4680
Add changeset
brophdawg11 Oct 4, 2023
1e8dcaa
Only leverage useViewTransitionState in DataRouter NavLink usages
brophdawg11 Oct 5, 2023
6ddc868
Fix tests
brophdawg11 Oct 11, 2023
04857aa
Unit tests
brophdawg11 Oct 11, 2023
e687cac
Bump bundle
brophdawg11 Oct 11, 2023
34ace81
Bump bundle
brophdawg11 Oct 11, 2023
54298be
Update changeset
brophdawg11 Oct 11, 2023
f3db06f
Rename viewTransitionOpts -> unstable_viewTransitionOpts
brophdawg11 Oct 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/start-view-transition.md
@@ -0,0 +1,46 @@
---
"react-router-dom": minor
"react-router": minor
"@remix-run/router": minor
---

Add support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) via `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application.

The simplest approach to enabling a View Transition in your React Router app is via the new `<Link unstable_viewTransition>` prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page.

If you need to apply more fine-grained styles for your animations, you can leverage the `unstable_useViewTransitionState` hook which will tell you when a transition is in progress and you can use that to apply classes or styles:

```jsx
function ImageLink(to, src, alt) {
let isTransitioning = unstable_useViewTransitionState(to);
return (
<Link to={to} unstable_viewTransition>
<img
src={src}
alt={alt}
style={{
viewTransitionName: isTransitioning ? "image-expand" : "",
}}
/>
</Link>
);
}
```

You can also use the `<NavLink unstable_viewTransition>` shorthand which will manage the hook usage for you and automatically add a `transitioning` class to the `<a>` during the transition:

```css
a.transitioning img {
view-transition-name: "image-expand";
}
```

```jsx
<NavLink to={to} unstable_viewTransition>
<img src={src} alt={alt} />
</NavLink>
```

For an example usage of View Transitions with React Router, check out [our fork](https://github.com/brophdawg11/react-router-records) of the [Astro Records](https://github.com/Charca/astro-records) demo.

For more information on using the View Transitions API, please refer to the [Smooth and simple transitions with the View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions/) guide from the Google Chrome team.
44 changes: 44 additions & 0 deletions docs/components/form.md
Expand Up @@ -5,6 +5,40 @@ new: true

# `<Form>`

<details>
<summary>Type declaration</summary>

```tsx
declare function Form(props: FormProps): React.ReactElement;

export interface LinkProps
extends React.FormHTMLAttributes<HTMLFormElement> {
method?: "get" | "post" | "put" | "patch" | "delete";
encType?:
| "application/x-www-form-urlencoded"
| "multipart/form-data"
| "text/plain";
action?: string;
relative?: "route" | "path";
preventScrollReset?: boolean;
onSubmit?: React.FormEventHandler<HTMLFormElement>;
reloadDocument?: boolean;
replace?: boolean;
state?: any;
unstable_viewTransition?: boolean;
}

type To = string | Partial<Path>;

interface Path {
pathname: string;
search: string;
hash: string;
}
```

</details>

The Form component is a wrapper around a plain HTML [form][htmlform] that emulates the browser for client side routing and data mutations. It is _not_ a form validation/state management library like you might be used to in the React ecosystem (for that, we recommend the browser's built in [HTML Form Validation][formvalidation] and data validation on your backend server).

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>
Expand Down Expand Up @@ -243,6 +277,14 @@ If you are using [`<ScrollRestoration>`][scrollrestoration], this lets you preve

See also: [`<Link preventScrollReset>`][link-preventscrollreset]

## `unstable_viewTransition`

The `unstable_viewTransition` prop enables a [View Transition][view-transitions] for this navigation by wrapping the final state update in `document.startViewTransition()`. If you need to apply specific styles for this view transition, you will also need to leverage the [`unstable_useViewTransitionState()`][use-view-transition-state].

<docs-warn>
Please note that this API is marked unstable and may be subject to breaking changes without a major release.
</docs-warn>

# Examples

TODO: More examples
Expand Down Expand Up @@ -349,3 +391,5 @@ You can access those values from the `request.url`
[scrollrestoration]: ./scroll-restoration
[link-preventscrollreset]: ./link#preventscrollreset
[history-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/state
[use-view-transition-state]: ../hooks//use-view-transition-state
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
47 changes: 46 additions & 1 deletion docs/components/link.md
Expand Up @@ -12,7 +12,7 @@ title: Link
```tsx
declare function Link(props: LinkProps): React.ReactElement;

interface LinkProps
export interface LinkProps
extends Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
"href"
Expand All @@ -23,6 +23,7 @@ interface LinkProps
reloadDocument?: boolean;
preventScrollReset?: boolean;
relative?: "route" | "path";
unstable_viewTransition?: boolean;
}

type To = string | Partial<Path>;
Expand Down Expand Up @@ -146,8 +147,52 @@ let { state } = useLocation();

The `reloadDocument` property can be used to skip client side routing and let the browser handle the transition normally (as if it were an `<a href>`).

## `unstable_viewTransition`

The `unstable_viewTransition` prop enables a [View Transition][view-transitions] for this navigation by wrapping the final state update in `document.startViewTransition()`:

```jsx
<Link to={to} unstable_viewTransition>
```

If you need to apply specific styles for this view transition, you will also need to leverage the [`unstable_useViewTransitionState()`][use-view-transition-state]:

```jsx
function ImageLink(to) {
let isTransitioning = unstable_useViewTransitionState(to);
return (
<Link to={to} unstable_viewTransition>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "",
}}
>
Image Number {idx}
</p>
<img
src={src}
alt={`Img ${idx}`}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "",
}}
/>
</Link>
);
}
```

<docs-warn>
Please note that this API is marked unstable and may be subject to breaking changes without a major release.
</docs-warn>

[link-native]: ./link-native
[scrollrestoration]: ./scroll-restoration
[history-replace-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
[history-push-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
[history-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/state
[use-view-transition-state]: ../hooks//use-view-transition-state
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
75 changes: 70 additions & 5 deletions docs/components/nav-link.md
Expand Up @@ -4,7 +4,11 @@ title: NavLink

# `<NavLink>`

A `<NavLink>` is a special kind of `<Link>` that knows whether or not it is "active" or "pending". This is useful when building a navigation menu, such as a breadcrumb or a set of tabs where you'd like to show which of them is currently selected. It also provides useful context for assistive technology like screen readers.
A `<NavLink>` is a special kind of `<Link>` that knows whether or not it is "active", "pending", or "transitioning". This is useful in a few different scenarios:

- When building a navigation menu, such as a breadcrumb or a set of tabs where you'd like to show which of them is currently selected
- It provides useful context for assistive technology like screen readers
- It provides a "transitioning" value to give you finer-grained control over [View Transitions][view-transitions]

```tsx
import { NavLink } from "react-router-dom";
Expand Down Expand Up @@ -42,8 +46,12 @@ The `className` prop works like a normal className, but you can also pass it a f
```tsx
<NavLink
to="/messages"
className={({ isActive, isPending }) =>
isPending ? "pending" : isActive ? "active" : ""
className={({ isActive, isPending, isTransitioning }) =>
[
isPending ? "pending" : "",
isActive ? "active" : "",
isTransitioning ? "transitioning" : "",
].join(" ")
}
>
Messages
Expand All @@ -57,10 +65,11 @@ The `style` prop works like a normal style prop, but you can also pass it a func
```tsx
<NavLink
to="/messages"
style={({ isActive, isPending }) => {
style={({ isActive, isPending, isTransitioning }) => {
return {
fontWeight: isActive ? "bold" : "",
color: isPending ? "red" : "black",
viewTransitionName: isTransitioning ? "slide" : "",
};
}}
>
Expand All @@ -74,7 +83,7 @@ You can pass a render prop as children to customize the content of the `<NavLink

```tsx
<NavLink to="/tasks">
{({ isActive, isPending }) => (
{({ isActive, isPending, isTransitioning }) => (
<span className={isActive ? "active" : ""}>Tasks</span>
)}
</NavLink>
Expand Down Expand Up @@ -112,4 +121,60 @@ When a `NavLink` is active it will automatically apply `<a aria-current="page">`

The `reloadDocument` property can be used to skip client side routing and let the browser handle the transition normally (as if it were an `<a href>`).

## `unstable_viewTransition`

The `unstable_viewTransition` prop enables a [View Transition][view-transitions] for this navigation by wrapping the final state update in `document.startViewTransition()`. By default, during the transition a `transitioning` class will be added to the `<a>` element that you can use to customize the view transition.

```css
a.transitioning p {
view-transition-name: "image-title";
}

a.transitioning img {
view-transition-name: "image-expand";
}
```

```jsx
<NavLink to={to} unstable_viewTransition>
<p>Image Number {idx}</p>
<img src={src} alt={`Img ${idx}`} />
</NavLink>
```

You may also use the `className`/`style` props or the render props passed to `children` to further customize based on the `isTransitioning` value.

```jsx
<NavLink to={to} unstable_viewTransition>
{({ isTransitioning }) => (
<>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "",
}}
>
Image Number {idx}
</p>
<img
src={src}
alt={`Img ${idx}`}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "",
}}
/>
</>
)}
</NavLink>
```

<docs-warn>
Please note that this API is marked unstable and may be subject to breaking changes without a major release.
</docs-warn>

[aria-current]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
[use-view-transition-state]: ../hooks//use-view-transition-state
10 changes: 10 additions & 0 deletions docs/hooks/use-navigate.md
Expand Up @@ -85,9 +85,19 @@ function EditContact() {
}
```

## `options.unstable_viewTransition`

The `unstable_viewTransition` option enables a [View Transition][view-transitions] for this navigation by wrapping the final state update in `document.startViewTransition()`. If you need to apply specific styles for this view transition, you will also need to leverage the [`unstable_useViewTransitionState()`][use-view-transition-state].

<docs-warn>
Please note that this API is marked unstable and may be subject to breaking changes without a major release.
</docs-warn>

[link]: ../components/link
[redirect]: ../fetch/redirect
[loaders]: ../route/loader
[actions]: ../route/action
[history-state]: https://developer.mozilla.org/en-US/docs/Web/API/History/state
[scrollrestoration]: ../components/scroll-restoration
[use-view-transition-state]: ../hooks//use-view-transition-state
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
2 changes: 1 addition & 1 deletion docs/hooks/use-submit.md
Expand Up @@ -150,7 +150,7 @@ submit(null, {
<Form action="/logout" method="post" />;
```

Because submissions are navigations, the options may also contain the other navigation related props from [`<Form>`][form] such as `replace`, `state`, `preventScrollReset`, `relative`, etc.
Because submissions are navigations, the options may also contain the other navigation related props from [`<Form>`][form] such as `replace`, `state`, `preventScrollReset`, `relative`, `unstable_viewTransition` etc.

[pickingarouter]: ../routers/picking-a-router
[form]: ../components/form
50 changes: 50 additions & 0 deletions docs/hooks/use-view-transition-state.md
@@ -0,0 +1,50 @@
---
title: unstable_useViewTransitionState
---

# `unstable_useViewTransitionState`

<details>
<summary>Type declaration</summary>

```tsx
declare function unstable_useViewTransitionState(
to: To,
opts: { relative?: "route" : "path" } = {}
): boolean;

type To = string | Partial<Path>;

interface Path {
pathname: string;
search: string;
hash: string;
}
```

</details>

This hook returns `true` when there is an active [View Transition][view-transitions] to the specified location. This can be used to apply finer-grained styles to elements to further customize the view transition. This requires that view transitions have been enabled for the given navigation via the [unstable_viewTransition][link-view-transition] prop on the `Link` (or the `Form`, `navigate`, or `submit` call).

Consider clicking on an image in a list that you need to expand into the hero image on the destination page:

```jsx
function NavImage({ src, alt, id }) {
let to = `/images/${idx}`;
let vt = unstable_useViewTransitionState(href);
return (
<Link to={to} unstable_viewTransition>
<img
src={src}
alt={alt}
style={{
viewTransitionName: vt ? "image-expand" : "",
}}
/>
</Link>
);
}
```

[link-view-transition]: ../components/link#unstable_viewtransition
[view-transitions]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
5 changes: 5 additions & 0 deletions examples/view-transitions/.gitignore
@@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
4 changes: 4 additions & 0 deletions examples/view-transitions/.stackblitzrc
@@ -0,0 +1,4 @@
{
"installDependencies": true,
"startCommand": "npm run dev"
}
14 changes: 14 additions & 0 deletions examples/view-transitions/README.md
@@ -0,0 +1,14 @@
---
title: View Transitions
toc: false
---

# startViewTransition (Experimental)

This example demonstrates a simple usage of a Data Router with `document.startViewTransition` enabled.

## Preview

Open this example on [StackBlitz](https://stackblitz.com):

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router/tree/brophdawg11/start-view-transition/examples/view-transitions?file=src/App.tsx)