Awesome landing page with Next.js & Scrollmagic

5 min read
React
Scrollmagic
Tutorial
Cover image for the blog post "Awesome Landing page with Next.js and Scrollmagic"

Summary

Check out the demo to see the final result.

In general, we coded a 2-column layout on desktop that breaks into a 1-column layout on smaller screens. The 2 column-layout has a "right lane" where images are faded into one another by scrolling.

This tutorial is designed for you to follow along with the demo repository.

Architecture

This project uses the following architecture:
Tip: You may be able to swap one dependency for another. For example, on my personal website I implemented the landing page without Tailwind. In case you're stuck, raise an issue at the repository of this article.

Looking at the config and dependencies

package.json

The file can be found here.
After cloning the repository, run
1npm install

tailwind.config.js

The file can be found here.
Don't forget to include the following segments:
  • purge should contain the path to your component and pages folder, and every other path you may use Tailwind class names in.
  • The variants block enables the :first-child selector for the opacity property - something that we will need later!
1// tailwind.config.js
2
3module.exports = {
4  // other config
5  purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
6  variants: {
7    extend: {
8      opacity: ['first'],
9    },
10  },
11}

postcss.config.js

The file can be found here.
Without its content, Next.js will not be able to pick up your Tailwind class names!

styles/main.css

The file can be found here.
For using Tailwind, the following imports are mandatory:
1/* styles/main.css */
2
3@tailwind base;
4@tailwind components;
5@tailwind utilities;
In Next.js projects, you have to import the stylesheet into your _app.tsx:
1// pages/_app.tsx
2
3import '../styles/main.css';

pages/_document.tsx

The file can be found here.
Important about it is the inclusion of jQuery and a custom jQuery plugin: Because of the custom plugin, we are not using the jquery npm package, but rather the script import, which can be found here. The custom plugin we are gonna need later can be found here.
1// pages/_document.tsx
2
3class MyDocument extends Document {
4  render() {
5    return (
6      <Html lang="en">
7        <Head>
8          <script defer src="/scripts/jQuery.min.js" />
9          <script defer src="/scripts/jQuery.inViewport.js" />
10        </Head>
11        // body etc.
12      </Html>
13    );
14  }
15}
Information: Our custom jQuery plugin exposes a new function called percentAboveBottom. This function tells us, how many percent a jQuery element is above the bottom of the viewport.

Example: When a element is completely inside the viewport, the function will return 1 (100%). When a element is half inside the viewport (measuring from the bottom of the screen), the function will return 0.5 (50%). And so on...
Tip: For type support of our jQuery plugin function percentAboveBottom, add this file to your project.

Describing the data

When we look at our demo, we can see that one section consists of the following areas:Image showing the architecture of our landing pageThis is why we're describing each section of our landing page using that same, intuitive approach. The data file can be found here. The following code snippet declaratively describes the first "index section". Our demo has 3 index sections, you can add as many as you want though!
1// data/indexSections.tsx
2
3export interface IIndexSection {
4  actionButton: ReactNode;
5  heading: string;
6  img: ImageProps;
7  tag?: keyof HTMLElementTagNameMap;
8  textContent: ReactNode;
9}
10
11const getIndexSections = (): IIndexSection[] => {
12  return [
13    {
14      heading: '1st heading',
15      img: {
16        src: '/img/1.png', // public/img/1.png is the path
17        alt: 'This is the first image',
18        priority: true,
19      },
20      textContent: (
21        <>
22          Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
23          nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
24          sed diam voluptua. At vero eos et accusam et justo duo dolores et ea
25          rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
26          ipsum dolor sit amet.
27        </>
28      ),
29      actionButton: <NextLink href="#">Read more</NextLink>,
30      tag: 'main',
31    },
32    ...
33  ];
34};
Important: Because of the nature of our implementation, all images have to be 1920x1080.
You can change from 1920x1080 to your desired dimensions by altering the following variable in the converter:
1// components/IndexSections/ToIndexSectionConverter.tsx
2
3const imgDimensions = {
4  width: 1920,
5  height: 1080,
6};
What remains is that all images have to have the same dimensions - when one is 1920x1080, the others have to be as well!
Tip: It is recommended that you apply tag: 'main' to the first index section. That way its wrapper receives the <main> tag. When you omit the tag in your data, the wrapper will receive the <section> tag.

Looking at the layout and hooks

components/IndexSections/ToIndexSectionConverter.tsx

The file can be found here.
This React component returns an array of React components. We will have to convert these later!
1// components/IndexSections/ToIndexSectionConverter.tsx
2
3const ToIndexSectionConverter = (props: IProps) => {
4  const { heading } = props;
5  return [
6    <LeftLaneItem key={heading} {...props} />,
7    <RightLaneItem key={heading} {...props} />,
8  ];
9};

components/IndexSections/IndexSections.tsx

The file can be found here.
Let's go over the logic step-by-step:

  • useAdaptLeftLaneItemHeight: This hook ensures that the index sections behave responsive in their height. The hook returns 2 values that are important for the react-scrollmagic <Scene> .
1// components/IndexSections/IndexSections.tsx
2
3const IndexSections = (/*...*/): JSX.Element => {
4  const container = useRef() as MutableRefObject<HTMLDivElement>;
5
6  // ...
7
8  const { leftLaneItemHeight, triggerHook } =
9    useAdaptLeftLaneItemHeight(container);
10
11  return (
12    <div className="index grid" ref={container}>
13      // ...
14        <Controller>
15          <Scene
16            duration={`${(leftLane.length - 1) * leftLaneItemHeight}`}
17            pin
18            triggerHook={triggerHook}
19          >
20            // ...
21          </Scene>
22        </Controller>
23      // ...
24    </div>
25  );
26};
  • useLeftAndRightLane: This hook maps over the data and converts them via the ToIndexSectionConverter. It uses lodash's zip function under the hood.
1// components/IndexSections/IndexSections.tsx
2
3const IndexSections = (/*...*/): JSX.Element => {
4  // ...
5
6  const [leftLane, rightLane] = useLeftAndRightLane();
7
8  // ...
9};
10
11// components/IndexSections/hooks.ts
12
13export const useLeftAndRightLane = () =>
14  useMemo(() => {
15    const pairs = getIndexSections().map(ToIndexSectionConverter);
16    return zip(...pairs); // [[left1, right1], [left2, right2]] => [[left1, left2], [right1, right2]]
17  }, []);
  • useOpacityChangeOnScroll: Is responsible for fading the next index section in. It uses our jQuery plugin percentAboveBottom under the hood.
1// components/IndexSections/IndexSections.tsx
2
3const IndexSections = ({
4  leftLaneTopOffset,
5  rightLaneStartCliff,
6}: IIndexSectionProps): JSX.Element => {
7  // ...
8
9  useOpacityChangeOnScroll({ leftLaneTopOffset, rightLaneStartCliff });
10
11  // ...
12};
Tip: You may wonder what leftLaneTopOffset and rightLaneStartCliff are there for. They are documented in the repository. In general, you can just leave their default values or play around with them until the animation meets your expectation.

A final note on styles

Even though Tailwind makes stylesheets almost obsolete, for things like setting the font-family or specifying the grid layout traditional styles were needed. Hence, don't forget to include the following styles in your style:
1/* styles/main.css */
2
3@tailwind base;
4@tailwind components;
5@tailwind utilities;
6
7body {
8    font-family: 'YOUR_FONT_FAMILY';
9}
10
11@screen md {
12    .index {
13        grid-template-columns: 1rem 40% 2rem 1fr 1rem;
14        grid-template-areas: ". left-lane . right-lane .";
15    }
16}

#next.js
#react
#scrollmagic
#animation
#pin
#layout
#goland
#tutorial

About the author

Boris Pöhland

Image of Boris Pöhland

Web Developer and Indie Indie Maker. Follow me on X


Comments

No comments yet!

What's on your mind?