Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active September 21, 2023 17:04
Show Gist options
  • Save ryanflorence/0432181409ebc96a201507621aee23e2 to your computer and use it in GitHub Desktop.
Save ryanflorence/0432181409ebc96a201507621aee23e2 to your computer and use it in GitHub Desktop.

View Transitions

Scroll Restoration

When the user has a history stack with element transitions, it's possible for them to scroll the element out of view on one or both pages. This can cause elements to translate thousands of pixels across the document and should be avoided.

Here are a few product-level use cases that I would consider good practice with a the proverbial grid-to-hero-image CSS transform.

Scroll Position Priority

G = Grid
H = Hero
IN = In View
OUT = Out of View

In this case, we avoid transforming unless both elements are visible and scroll position is always maintained.

navigation transition
G IN -> H IN transform
H IN -> G IN transform
G IN -> H OUT fade
H IN -> G OUT fade
H OUT -> G OUT fade
G OUT -> H OUT fade
H OUT -> G IN fade
G OUT -> H IN fade

Element Visibility Priority

Here we prioritize the visibility of the destination element at the expense of scroll position. Note we never scroll the origin page.

sIV = scrollIntoView()

navigation transition
G IN -> H IN transform
H IN -> G IN transform
G IN -> H OUT sIV -> transform
H IN -> G OUT sIV -> transform
H OUT -> G OUT siV -> fade
G OUT -> H OUT sIV -> fade
H OUT -> G IN fade
G OUT -> H IN fade

useViewTransition

function ImagesNav() {
  let images = useLoaderData();
  return (
    <div>
      {images.map(image => (
        <NavImage key={image.id} image={image} />
      ))}
    </div>
  );
}

function NavImage({
  img,
}: {
  img: { id: string; title: string; src: string };
}) {
  let href = `/movies/${img.id}`;
  let vt = useViewTransition(href);

  return (
    <div>
      <p style={{ viewTransitionName: vt ? "my-movie-title" : "" }}>
        {image.title}
      </p>
      <Link to={href}>
        <img
          src={image.src}
          style={{ viewTransitionName: vt ? "my-movie-image" : "" }}
        />
      </Link>
    </div>
  );
}
function MoviePage(img) {
  let movie = useLoaderData();

  return (
    <main>
      <h1>{img.title}</h1>
      <img src={img.src} style={{ viewTransitionName: "my-movie-title" }} />
      <p style={{ viewTransitionName: "my-movie-image" }}>
        {movie.description}
      </p>
    </main>
  );
}

<NavLink className style children />

function NavImage({
  img,
}: {
  img: { id: string; title: string; src: string };
}) {
  let href = `/movies/${img.id}`;
  let vt = useViewTransition(href);

  return (
    <NavLink unstable_transitions>
      {({ isActive, isPending, isTransitioning }) => (
        <>
          <p style={{ viewTransitionName: vt ? "my-movie-title" : "" }}>
            {image.title}
          </p>
          <img
            src={image.src}
            style={{ viewTransitionName: vt ? "my-movie-image" : "" }}
          />
        </>
      )}
    </NavLink>
  );
}

function NavImage({
  img,
}: {
  img: { id: string; title: string; src: string };
}) {
  let href = `/movies/${img.id}`;
  let vt = useViewTransition(href);

  return (
    <NavLink
      unstable_transitions
      className={({ isPending, isTransitioning }) =>
        isTransitioning ? "animate" : ""
      }
    >
      <p>{image.title}</p>
      <img src={image.src} />
    </NavLink>
  );
}

useGlobalViewTransition

Need to go crazy? Drop down to a lower API

useViewTransitions(({ currentLocation, nextLocation }) => {
  // do stuff now
  let match = matchPath("/movies/:id", nextLocation.pathname);
  return transition => {
    transition.ready;
    transition.finished;
    // do stuff later
  };
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment