Show Interactive Features on Landing Page with React

November 1, 2022

3 min read

Show Interactive Features on Landing Page with React
Watch on YouTube

Let me share a setup for displaying live product features on the landing page so you can get interesting bits for your React project.

landing

Here we have a few slices that show a preview of the feature on one side and information on another. To display these blocks, I use the LandingFeatureSlice component that receives the title, description, call to action, a function to render a preview, and startsWith parameter so we can alternate slices.

import { ReactNode } from "react"
import { reverseIf } from "shared/utils/reverseIf"
import styled from "styled-components"
import { IntersectionAware } from "ui/IntersectionAware"
import { VStack } from "ui/Stack"
import { Text } from "ui/Text"

import { LandingSlice } from "./LandingSlice"

type StartsWith = "preview" | "info"

interface Props {
  title: ReactNode
  description: ReactNode
  cta: ReactNode
  renderPreview: () => ReactNode
  startsWith: StartsWith
}

const Wrapper = styled(LandingSlice)`
  padding: 40px 0;
  min-height: 100vh;
`

const Container = styled.div<{ isInfoFirst: boolean }>`
  display: grid;
  grid-gap: 40px;
  align-items: center;
  grid-template-columns: ${({ isInfoFirst }) =>
    reverseIf(["3fr", "2fr"], isInfoFirst).join(" ")};

  > * {
    :last-child {
      justify-self: center;
    }
  }

  @media (max-width: 768px) {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(200px, 1fr);
  }
`

export const LandingFeatureSlice = ({
  title,
  description,
  renderPreview,
  startsWith,
  cta,
}: Props) => {
  const info = (
    <VStack key="info" alignItems="start" gap={40}>
      <Text height="large" weight="bold" size={32} as="h2">
        {title}
      </Text>
      <VStack gap={8}>{description}</VStack>
      {cta}
    </VStack>
  )

  const isInfoFirst = startsWith === "info"

  return (
    <IntersectionAware<HTMLDivElement>
      render={({ ref, isIntersecting }) => {
        const content = reverseIf(
          [isIntersecting ? renderPreview() : null, info],
          isInfoFirst
        )

        return (
          <Wrapper ref={ref}>
            <Container isInfoFirst={isInfoFirst}>{content}</Container>
          </Wrapper>
        )
      }}
    />
  )
}

As a root container, we use the LandingSlice component that takes the whole width of the page and creates spacing on the sides using a CSS grid, so we can allow some children to take the full width of the page by changing it's grid column attribute only. Then we render a container to show the preview on one side and information on another. Here we want to give more space for the interactive part and center second children while keeping the first element centered on the start for better esthetics.

Showing a preview of the feature could slow down the page. We don't want that because it will lower our score at google page speed insights, and it would lead to less traffic through SEO. It is good to have intersection observer API, so we can skip rendering of the preview while it is far from appearing on the view.

These features require context about the user to work well. And here comes the beauty of React Contexts, where we can have different provides for specific situations. I use UserStateProvider for a signed-in user. It queries the state from the back end using react-query, while for the landing page, I have the LandingUserStateProvider that gives a stub version of the user state.

Finally, we don't want to make those features fully interactive because it requires a mutation state on the server side. That's why we have the PresentationProvider with the onInteraction hook to show a sing up modal when the user interacts with the preview.