Search code examples
javascripttypescriptpreactpreact-router

how can i create a page transition with preact


I'm trying to get a page transition using preact-router, i already tried the preact-transition-group package with the preact-css-transition-group package, that gives me an error, anyways here is my basic setup:

import { h, FunctionComponent } from 'preact';
import Router from 'preact-router';

const App: FunctionComponent = () => {
  return (
    <Router>
      <div path="/1" style="padding: 50px">
        page 1 <a href="/2">page 2</a>
      </div>
      <div path="/2" style="padding: 50px">
        page 2 <a href="/1">page 1</a>
      </div>
    </Router>
  );
};

a typescript and/or preact-router solution is preferable but not necessary.


Solution

  • Note: that this approach probably dosn't work with nested routes.

    With this approach you need to wrap every route in a div with a class that will animate the route and set the key and path attribute to the path of the route like this:

    function wrapRoute(route: h.JSX.Element): h.JSX.Element {
      return ( // the in class is the animation and the page class is makes sure that the old and new routes overlap
        <div path={route.props.path} class="in page" key={route.props.path}>
          {route}
        </div>
      );
    }
    

    then you need to add two reactive variables like this:

    const [previousEl, setPreviousEl] = useState<ComponentChildren | null>(null);
    const [outEl, setOutEl] = useState<JSX.Element | null>(null);
    

    after that you can listen to the onChange event of the preact-router and set the outEl to a div that wraps previousEl and has a class that will animate the exit of the route, then add a onAnimationEnd listener so that you can set the outEl to null one the out animation has finished, last thing you need to set the previousEl to e.current.props.children, your router component listener should look like this:

    <Router onChange={(e) => {
      if (previousEl) {
        setOutEl(
          <div class="out page" key={e.previous} onAnimationEnd={() => setOutEl(null)}>
            {previousEl}
          </div>
        );
      }
      if (e.current) {
        setPreviousEl(e.current.props.children);
      }
    }}
    >
    {routes} // this is an array containing all the wrapped routes.
    </Router>
    

    last thing you need to do is to create the animations, look at the app.sass example below.

    here is the full example:

    app.tsx

    import { h, FunctionComponent, JSX, ComponentChildren, Fragment } from 'preact';
    import Router from 'preact-router';
    import { useState } from 'preact/hooks';
    import './app.sass'
    
    const routes = [
      <div style="padding: 50px; background: red" path="/1">
        page 1 <a href="/2">page 2</a> <a href="/3">page 3</a>
      </div>,
      <div style="padding: 50px; background: blue" path="/2">
        page 2 <a href="/1">page 1</a> <a href="/3">page 3</a>
      </div>,
      <div style="padding: 50px; background: green" path="/3">
        page 2 <a href="/1">page 1</a> <a href="/2">page 2</a>
      </div>,
    ].map((route) => wrapRoute(route));
    
    function wrapRoute(route: h.JSX.Element): h.JSX.Element {
      return (
        <div path={route.props.path} class="in page" key={route.props.path}>
          {route}
        </div>
      );
    }
    
    const App: FunctionComponent = () => {
      const [previousEl, setPreviousEl] = useState<ComponentChildren | null>(null);
      const [outEl, setOutEl] = useState<JSX.Element | null>(null);
    
      return (
        <Fragment>
          <Router
            onChange={(e) => {
              if (previousEl) {
                setOutEl(
                  <div class="out page" key={e.previous} onAnimationEnd={() => setOutEl(null)}>
                    {previousEl}
                  </div>
                );
              }
              if (e.current) {
                setPreviousEl(e.current.props.children);
              }
            }}
          >
            {routes}
          </Router>
          {outEl}
        </Fragment>
      );
    };
    
    export default App;
    
    

    app.sass

    .page
      position: absolute
      top: 0
      bottom: 0
      left: 0
      right: 0
    
    .out
      animation: out 200ms forwards
    
    @keyframes out
      0%
        opacity: 1
        transform: translateX(0)
      100%
        opacity: 0
        transform: translateX(100vw)
    
    .in
      animation: in 200ms forwards
    
    @keyframes in
      0%
        opacity: 0
        transform: translateX(-100vw)
      100%
        opacity: 1
        transform: translateX(0)