{"id":16175603,"url":"https://github.com/johnpolacek/tweenpages","last_synced_at":"2025-03-16T10:31:12.592Z","repository":{"id":40684413,"uuid":"403776543","full_name":"johnpolacek/TweenPages","owner":"johnpolacek","description":"Build complex animated page transitions with GSAP and Next.js","archived":false,"fork":false,"pushed_at":"2022-08-01T13:22:10.000Z","size":986,"stargazers_count":187,"open_issues_count":3,"forks_count":32,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-10-11T04:45:14.249Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"tweenpages.vercel.app","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnpolacek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-06T22:51:16.000Z","updated_at":"2024-10-06T23:53:36.000Z","dependencies_parsed_at":"2022-08-10T00:40:45.237Z","dependency_job_id":null,"html_url":"https://github.com/johnpolacek/TweenPages","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnpolacek%2FTweenPages","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnpolacek%2FTweenPages/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnpolacek%2FTweenPages/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnpolacek%2FTweenPages/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnpolacek","download_url":"https://codeload.github.com/johnpolacek/TweenPages/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221662727,"owners_count":16859736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T04:45:16.233Z","updated_at":"2024-10-27T10:25:27.299Z","avatar_url":"https://github.com/johnpolacek.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Overview\n\nTweenPages is on Github: [github.com/johnpolacek/TweenPages](https://github.com/johnpolacek/TweenPages)\n\n### Why Next.js?\n\nOne of the things that makes Next.js so great is that you can create a modern, slick single page app experience while keeping the performance and other benefits of classic server-side rendering and routing.\n\nThere is an official [next-page-transitions example](https://github.com/vercel/next.js/tree/canary/examples/with-next-page-transitions) that demonstrates how to add a loading state when navigating to a page, wait for the page to load its content and animate in when it's ready.\n\nWhat about something more ambitious and complex? We could offer something more ambitious:\n\n- Intro and outro animations defined at the component level\n- A higher order component that manages page transitions and the animations of nested child components\n- Re-usable, declarative animation componenents\n- No flash of unstyled content on initial page load\n\n### Why GSAP?\n\nBack in the early days of the web, if you wanted to craft complex, ambitious animations as part of a web experience, you would use Flash. Thanks to advancements in the web platform and open source project, we can build these today, but in an accessible, responsive, performant way.\n\nThe Greensock Animation Platform was the best animation library in Flash and is today the most robust animation library for the web, benefiting from many years of enhancements and platform stability.\n\nA great place to get started with GSAP and React is to read [GSAP + React, First Steps \u0026 Handy Techniques](https://greensock.com/react) and [GSAP + React, Advanced Animation Techniques](https://greensock.com/react-advanced).\n\n## Intros and Outros\n\n### The Intro/Outro Pattern\n\nA useful pattern in building animations into applications is the concept of component-level intro/outro animations, something that GSAP is very well suited for. \n\nEach component can have its own animation as it gets added to the view, an intro. Typically you would do this by defining styles for the starting values for an element, such as `opacity:0` for a fade in. Then setting another style for the ending value when you element comes to rest (e.g. `opacity:1`).\n\nYour components may be composed of multiple elements, with different values that get transitioned, perhaps flying in, rotating, scaling, etc.\n\nThe outro is typically the intro animation in reverse, where you set a value that you want the element to transition to before it is removed from the view (e.g. `opacity:1`. \n\nIt is of course possible you may wish to mix and match these intro/outro animations, for example a fly in from the bottom of the view, then a static fade out for the exit.\n\n### Intro Animations in Next.JS\n\nFor an intro animations in React, you might typically reach for `useLayoutEffect`, but because Next.js does SSR, your console will fill up with warnings for every animated component you have on your page. To avoid this we can apply `useIsomorphicLayoutEffect` instead of `useEffect`. [Check it out on Github](https://github.com/streamich/react-use/blob/master/docs/useIsomorphicLayoutEffect.md).\n\nAdditionally with SSR, to prevent the flash of unstyled content (FOUC). This frequently happens when an element on a server-side rendered page displays for a brief moment while the JavaScript is loaded and executed.\n\nFor example, if you have a fade intro on your element, there will be a brief flash where it is displayed at full opacity, then it will disappear and fade in as intended.\n\nTo avoid this, we need to make sure the initial styling state of the component is correct. For example, if we are fading in, the initial style of that component should be an opacity of zero.\n\n### Outro Animations in Next.JS\n\nOutro animations are where it gets tricky. We need to intercept the page transition, and do whatever exit animations our child components need to do, then proceed to go to the next page, where our components will all animate in.\n\nTo pull this off, we will make use of the following:\n\n- A `TransitionLayout` higher order component used as a wrapper in `MyApp` that will delay the routing change until after any animations have completed.\n- A `TimelineProvider` component that will take advantage of React’s `useContext` hook to share an outro timeline across multiple components, wherever they are nested in our app\n\n## Page Transitions\n\nIn Next.js, we can add a custom App component to initialize pages. It is typical to use this to persist layouts between page changes (e.g. a header navigation bar).\n\nAdditionally, we can use the Custom App component to manage our page transition animations. \n\n### Transition Provider\n\nIn order to make a page transition effect, we need to prevent rendering the new page before our outro animation is done. \n\nWe may have many components with different animation effects nested in our pages. To keep track of all the different outro transitions, we will use a combination of React’s Context API and a top-level GSAP timeline.\n\nIn `TransitionContext` we will create our `TransitionProvider` which will make our GSAP timeline for outro animations available to any components who would like to transition out during a page change.\n\n```\nimport React, { useState, createContext, useCallback } from \"react\"\nimport gsap from \"gsap\"\n\nconst TransitionContext = createContext({})\n\nconst TransitionProvider = ({ children }) =\u003e {\n  const [timeline, setTimeline] = useState(() =\u003e\n    gsap.timeline({ paused: true })\n  )\n\n  return (\n    \u003cTransitionContext.Provider\n      value={{\n        timeline,\n        setTimeline,\n      }}\n    \u003e\n      {children}\n    \u003c/TransitionContext.Provider\u003e\n  )\n}\n\nexport { TransitionContext, TransitionProvider }\n```\n\nWhat if you have pages with different background colors and you would like to transition smoothly from one to another? We can can add `background` as another property in addition to `timeline` in `TransitionContext`.\n\n### Transition Layout\n\nNext, we have `TransitionLayout` which will be our controller that will initiate the outro animations and update the page when they are all complete. It also contains a wrapper component for the background color page transition animation.\n\n```\nimport { gsap } from \"gsap\"\nimport { TransitionContext } from \"../context/TransitionContext\"\nimport { useState, useContext, useRef } from \"react\"\nimport useIsomorphicLayoutEffect from \"../animation/useIsomorphicLayoutEffect\"\n\nexport default function TransitionLayout({ children }) {\n  const [displayChildren, setDisplayChildren] = useState(children)\n  const { timeline, background } = useContext(TransitionContext)\n  const el = useRef()\n\n  useIsomorphicLayoutEffect(() =\u003e {\n    if (children !== displayChildren) {\n      if (timeline.duration() === 0) {\n        // there are no outro animations, so immediately transition\n        setDisplayChildren(children)\n      } else {\n        timeline.play().then(() =\u003e {\n          // outro complete so reset to an empty paused timeline\n          timeline.seek(0).pause().clear()\n          setDisplayChildren(children)\n        })\n      }\n    }\n  }, [children])\n\n  useIsomorphicLayoutEffect(() =\u003e {\n    gsap.to(el.current, {\n      background,\n      duration: 1,\n    })\n  }, [background])\n\n  return \u003cdiv ref={el}\u003e{displayChildren}\u003c/div\u003e\n}\n```\n\n### Custom App\n\nLet’s take a look at our Custom App component.\n\n```\nimport { TransitionProvider } from \"../src/context/TransitionContext\"\nimport TransitionLayout from \"../src/animation/TransitionLayout\"\nimport { Box } from \"theme-ui\"\nimport Header from \"../src/ui/Header\"\nimport Footer from \"../src/ui/Footer\"\n\nexport default function MyApp({ Component, pageProps }) {\n  return (\n    \u003cTransitionProvider\u003e\n      \u003cTransitionLayout\u003e\n        \u003cBox\n          sx={{\n            display: \"flex\",\n            minHeight: \"100vh\",\n            flexDirection: \"column\",\n          }}\n        \u003e\n          \u003cHeader /\u003e\n          \u003cComponent {...pageProps} /\u003e\n          \u003cFooter /\u003e\n        \u003c/Box\u003e\n      \u003c/TransitionLayout\u003e\n    \u003c/TransitionProvider\u003e\n  )\n}\n```\n\nHere we have `TransitionProvider` and `TransitionLayout` wrapping the other elements so that they can access our `TransitionContext` properties. We have a `Header` and `Footer` that exist outside of `Component` so that they will be static after the initial page load.\n\n### Component-Level Animation\n\nHere is an example of a basic animation we can do at the component level. We can add as many of these as we want to a page and they will all do the same thing, wrap all its children in a transparent div and fade it in on page load, then fade out when navigating to a different page.\n\n```\nimport { useRef, useContext } from \"react\"\nimport { gsap } from \"gsap\"\nimport { Box } from \"theme-ui\"\nimport useIsomorphicLayoutEffect from \"./useIsomorphicLayoutEffect\"\nimport { TransitionContext } from \"../context/TransitionContext\"\n\nconst FadeInOut = ({ children }) =\u003e (\n  const { timeline } = useContext(TransitionContext)\n  const el = useRef()\n\n  // useIsomorphicLayoutEffect to avoid console warnings\n  useIsomorphicLayoutEffect(() =\u003e {\n    // intro animation will play immediately\n    gsap.to(el.current, {\n      opacity: 1,\n      duration: 1,\n    })\n\n    // add outro animation to top-level outro animation timeline\n    timeline.add(\n      gsap.to(el.current, {\n        opacity: 0,\n        duration: .5,\n      }),\n      0\n    )\n  }, [])\n\n  // set initial opacity to 0 to avoid FOUC for SSR\n  \u003cBox ref={el} sx={{opacity: 0}}\u003e\n    {children}\n  \u003c/Box\u003e\n)\n\nexport default FadeInOut\n```\n\nWe can take this pattern and extract it into an extendable `AnimateInOut` helper component for reusable intro/outro animation patterns in our app.\n\n```\nimport React, { useRef, useContext } from \"react\"\nimport { gsap } from \"gsap\"\nimport { Box } from \"theme-ui\"\nimport useIsomorphicLayoutEffect from \"./useIsomorphicLayoutEffect\"\nimport { TransitionContext } from \"../context/TransitionContext\"\n\nconst AnimateInOut = ({\n  children,\n  as,\n  from,\n  to,\n  durationIn,\n  durationOut,\n  delay,\n  delayOut,\n  set,\n  skipOutro,\n}) =\u003e {\n  const { timeline } = useContext(TransitionContext)\n  const el = useRef()\n\n  useIsomorphicLayoutEffect(() =\u003e {\n    // intro animation\n    if (set) {\n      gsap.set(el.current, { ...set })\n    }\n    gsap.to(el.current, {\n      ...to,\n      delay: delay || 0,\n      duration: durationIn,\n    })\n\n    // outro animation\n    if (!skipOutro) {\n      timeline.add(\n        gsap.to(el.current, {\n          ...from,\n          delay: delayOut || 0,\n          duration: durationOut,\n        }),\n        0\n      )\n    }\n  }, [])\n\n  return (\n    \u003cBox as={as} sx={from} ref={el}\u003e\n      {children}\n    \u003c/Box\u003e\n  )\n}\n\nexport default React.memo(AnimateInOut)\n```\n\nThe `AnimateInOut` component has built in flexibility for different scenarios:\n\n- Setting different animations, durations and delays for intros and outros\n- Skipping the outro\n- Setting the element tag for the wrapper, e.g. use a `\u003cspan\u003e` instead of a `\u003cdiv\u003e`\n- Use GSAP’s `set` option to define initial values for the intro\n\nUsing this we can create all sorts of reusable intro/outro animations, such as `\u003cFlyInOut\u003e`, `\u003cScaleInOut\u003e`, `\u003cRotateInOut3D\u003e` and so forth.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnpolacek%2Ftweenpages","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnpolacek%2Ftweenpages","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnpolacek%2Ftweenpages/lists"}