Skip to content

Commit

Permalink
Add startViewTransition support (#10916)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Oct 11, 2023
1 parent f77743a commit feebfc0
Show file tree
Hide file tree
Showing 30 changed files with 3,003 additions and 75 deletions.
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)

0 comments on commit feebfc0

Please sign in to comment.