Skip to content

mattcroat/sveltekit-view-transition

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sveltekit-view-transition

Simplified view-transition-api for Sveltekit.

MIT License GitHub last commit npm npm npm bundle size

Before we begin: what is a view transition?

I could go in thousands of details on what view-transitions are but what a better way of understanding them that from the words of Jake Archibald, the main mind behind them? Here's a wonderful article from him that explains what they are and how to use them in Vanilla JS.

Installation

npm i -D sveltekit-view-transition@latest  # or pnpm/yarn

Usage

The simplest setup:

src/routes/+layout.svelte

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	setupViewTransition();
</script>

<slot />

This will automatically enable the default transition to every navigation.

Modifying the defaults:

Optionally, the default animation can be modified via ::view-transition-old(root) and ::view-transition-new(root) like so:

src/routes/+layout.svelte

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	setupViewTransition();
</script>

<slot />

<style>
	/* Disable default crossfade. */
	:root {
		view-transition-name: none;
	}

	/* Or, just modify the duration. */
	:gloabl(::view-transition-old(root)),
	:gloabl(::view-transition-new(root)) {
		animation-duration: 2s;
	}
</style>

transition

It's often useful to give specific parts of the page their own unique view transitions. We can do this by setting an element's view-transition-name.

One way to do this is with transition, a svelte action returned by setupViewTransition.

transition accepts a string representing the view-transition-name that should be assigned to the element using it:

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	const { transition } = setupViewTransition();
</script>

<header use:transition={'header'}>
	<!-- links -->
</header>

<slot />

<style>
	:global(::view-transition-old(header)),
	:gloibal(::view-transition-new(header)) {
		/* ... */
	}
</style>

As you can see, the header's view transition can now be modified via ::view-transition-old(header) and ::view-transition-new(header).

Additional Options

If you want to be a bit more creative with the transitions, you can pass an object to the transition action instead of a string. This object accepts the following options:

  • name: the view-transition-name (required)
  • classes: classnames to apply to the target element during the transition. An array of strings or a function that returns one.
  • applyImmediately: Whether the transition should be applied immediately, or only during the actual navigation. A boolean or a function that returns one.
  • shouldApply: Whether the transition should be applied or not. Can be a boolean or a function that returns one.

Let's take a look at each of these options in more detail:

name

The view-transition-name -- the only required parameter. It can be a string or a function that takes a navigation object and returns a string, and will be applied to the target element during the transition. This is equivalent to setting the style property view-transition-name on an element.

classes

Either an array of strings, or a function that returns an array of strings. These classes will be applied as classnames to the root element during the transition.

To demonstrate this, let's assume we want to apply a unique transition to our header anytime our "back" button is clicked.

This can be achieved by returning an array, i.e. ["back"], to our classes callback.

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	const { transition } = setupViewTransition();
</script>

<header
	use:transition={{
		name: 'header',
		classes({ navigation }) {
			if (navigation.to?.route?.id === '/') {
				return ['back'];
			}
		},
	}}
>
	<a href="/">Back</a>
</header>

<!-- etc -->

Now, we can target .back::view-transition-old(back) and .back::view-transition-new(back) in our CSS and those transitions will only be applied when navigating to the home page /.

In the example above, you can see we're destrucuring navigation from the provided OnNavigate object (the same object that sveltekit will pass to the onNavigate function). This object contains a lot of useful information, including the page you are navigating to, allowing us to apply classes conditionally based on the navigation.

Click here to see the full Navigation interface.
interface Navigation {
	/**
	 * Where navigation was triggered from
	 */
	from: {
		/**
		 * Parameters of the target page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object.
		 * Is `null` if the target is not part of the SvelteKit app (could not be resolved to a route).
		 */
		params: Record<string, string> | null;
		/**
		 * Info about the target route
		 */
		route: { id: string | null };
		/**
		 * The URL that is navigated to
		 */
		url: URL;
	} | null;
	/**
	 * Where navigation is going to/has gone to
	 */
	to: {
		/**
		 * Parameters of the target page - e.g. for a route like `/blog/[slug]`, a `{ slug: string }` object.
		 * Is `null` if the target is not part of the SvelteKit app (could not be resolved to a route).
		 */
		params: Record<string, string> | null;
		/**
		 * Info about the target route
		 */
		route: { id: string | null };
		/**
		 * The URL that is navigated to
		 */
		url: URL;
	} | null;
	/**
	 * The type of navigation:
	 * - `form`: The user submitted a `<form>`
	 * - `leave`: The user is leaving the app by closing the tab or using the back/forward buttons to go to a different document
	 * - `link`: Navigation was triggered by a link click
	 * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect
	 * - `popstate`: Navigation was triggered by back/forward navigation
	 */
	type: 'form' | 'leave' | 'link' | 'goto' | 'popstate';
	/**
	 * Whether or not the navigation will result in the page being unloaded (i.e. not a client-side navigation)
	 */
	willUnload: false;
	/**
	 * In case of a history back/forward navigation, the number of steps to go back/forward
	 */
	delta?: number;
	/**
	 * A promise that resolves once the navigation is complete, and rejects if the navigation
	 * fails or is aborted. In the case of a `willUnload` navigation, the promise will never resolve
	 */
	complete: Promise<void>;
}

applyImmediately

By default, the transition name you provide will only be applied during the actual navigation, following the suggestion from Jake Archibald himself (the creator of the view transition api), which states that you shouldn't add transition names to everything -- instead, only to the elements involved in the transition. However, sometimes you want to add a transition name immediately (for example, when you're navigating back from a "detail" page and you want to animate back an image in the list).

applyImmediately is either a boolean or a function that take the navigation object (please note that this is the navigation object from the previous page, so the from will be the page that is navigating to the current page, and the to will be the current page) and returns a boolean.

Here's a simple example of this in action:

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	const { transition } = setupViewTransition();

	export let data;
</script>

<ul>
	{#each data.posts as post (post.id)}
		<li
			use:transition={{
				/**
				 * in the post/[id] page we have a matching title transition name
				 */
				name: 'title',
				applyImmediately(navigation) {
					// this will apply the title view transition to this element
					// only if it's the one that matches the id we are coming from
					// so for example if we were visiting /post/1 and this is the
					// post with id 1. Notice that i'm checking the `from` because
					// this will actually be called when the navigation is still happening
					// from post/1 to /
					return navigation?.from?.params?.id === post.id.toString();
				},
			}}
		>
			<a href="/post/{post.id}">{post.title}</a>
		</li>
	{/each}
</ul>

In this example, when we navigate back from the /post/1 page, the title will slide into the its position in the list.

Important Note: the transition name will be added before the transition ends and removed immediately after to allow for a forward transition from another post to happen. If not removed the transition name would be duplicated.

shouldApply

As mentioned above, this can be either a boolean or a function that takes a navigation object (this time the from is this page and the to is the page you are navigating to) and returns a boolean. If the return value is true the transition name will be applied, otherwise it will not. This is useful when, for example, you want to navigate from a list to a detail.

NB: the default is true so if you don't pass shouldApply the transition name will be applied every time.

So, completing the example above:

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	const { transition } = setupViewTransition();

	export let data;
</script>

<ul>
	{#each data.posts as post (post.id)}
		<li
			use:transition={{
				/**
				 * in the post/[id] page we have a matching title transition name. Note that
				 * the transition name will only be applied moments before the actual transition
				 * if and when the shouldApply function returns true
				 */
				name: 'title',
				applyImmediately(navigation) {
					return navigation?.from?.params?.id === post.id.toString();
				},
				shouldApply(navigation) {
					// if the params.id i'm navigating to is equal to the id of the post
					// we add the title transition name.
					return navigation?.to?.params?.id === post.id.toString();
				},
			}}
		>
			<a href="/post/{post.id}">{post.title}</a>
		</li>
	{/each}
</ul>

More advanced usage

on

This function is returned from setupViewTransition, and allows you to add a listener to run code during an arbitrary moment of the "lifecycle" of the view transition. It takes three arguments; the name of the event, the main callback function, and a series of options.

option name type meaning
registerDuringTransition boolean Wether the callback should be added immediately (even if there's a transition running) or not. This is because there are events that runs after the component has mounted, and if you add the listeners on mount specifying true as the third argument, this listeners will be called immediately. This might be useful if, for example, you want to modify the state after an incoming transition.
avoidWrapping boolean By default the function will be internally wrapped in afterNavigate (to reassign the listeners even if the navigation is towards the same page) but you can pass a forth parameter as true to avoid wrapping the add listener in afterNavigate.
autoClean boolean wether the listener clean automatically after has been applied or it requires manual cleaning. It defaults to true

There are 7 types of events you can subscribe to in order of calling:

Event Name Description
before-start-view-transition event called before the transition even start if you modify the DOM here it will be included in the "screenshot" of the view-transition
before-navigation Event called before SvelteKit start to handle the navigation the view transition has
already been started
before-navigation-complete Event called after SvelteKit started to handle the navigation but before it completes.
The view-transition is still happening
after-navigation-complete Event called after the navigation from SvelteKit has completed.
The view-transition is still happening
transition-ready Event called when the view-transition is ready, the pseudo-elements are created.
update-callback-done Event called when the callback of the view-transition has finished running.
transition-finished Event called when the view-transition finish and the new view is in place

The on function also returns a function that unregister the callback when called.

off

A function to unsubscribe a specific handle from a specific event. This will rarely be necessary given that the on function already returns unsubscribe.

By default the function will be internally wrapped in afterNavigate (to reassign the listeners even if the navigation is towards the same page) but you can pass a third parameter as true to avoid wrapping the remove listener in afterNavigate.

classes

Much like the classes function on the action, this function can be called immediately in the script tag of a component to add a specific class to the :root element during a navigation. It can either be a array of strings or a function that returns an array of strings. The callback provides a navigation object (just like the one from the action).

By default the function will be internally wrapped in afterNavigate (to reassign the listeners even if the navigation is towards the same page) but you can pass a second parameter as true to avoid wrapping the add listener in afterNavigate.

<script>
	import { setupViewTransition } from 'sveltekit-view-transition';

	const { classes } = setupViewTransition();

	classes(({ navigation }) => {
		if (navigation.to?.route.id === '/') {
			return ['back'];
		}
	});
</script>

Examples

You can find some example of usage in the examples folder.

Example Sveltelab Features
list-and-details link entry/exit animation, dynamic name, page transition from list to detail

Contributing

Contributions are always welcome!

For the moment there's no code of conduct or contributing guideline, but if you've found a problem or have an idea, feel free to open an issue

For the fastest way to open a PR, try out Codeflow:

Open in Codeflow

Authors

About

Start using view transitions without any hassle!

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 80.4%
  • Svelte 9.8%
  • JavaScript 6.2%
  • CSS 2.0%
  • HTML 1.6%