Scroll Reveal Content A

PreviousNext

A block that shows 3 parts of content with a specific image for each part, and a fancy line grow expand during scroll.

Docs
abuiblock

Preview

Loading preview…
registry/abui/marketing/scroll-reveal-content-a.tsx
"use client"

import React from "react"
import { useRef } from "react"
import { cn } from "@/lib/utils"
import Image from "next/image"
import { useMotionValueEvent, useScroll } from "motion/react"

export const centralColumnStyle = "w-[90%] max-w-[1340px] mx-auto"
export const pageYPadding = "py-10 md:py-12 lg:py-20 xl:py-30 2xl:py-40"
const defaultTitleClass = "text-2xl md:text-3xl font-semibold mb-2 text-foreground"
const defaultDescriptionClass = "text-base md:text-lg font-medium mb-2 text-foreground max-w-[400px] leading-[130%]"
const imageClass =
  "absolute top-0 right-0 ml-auto w-auto h-full object-cover rounded-2xl transition-opacity duration-300"

export interface ItemContent {
  title: string
  description: string
  image: {
    url: string
    width: number
    height: number
    alt: string
  }
}

interface Props extends React.ComponentProps<"div"> {
  contentA: ItemContent
  contentB: ItemContent
  contentC: ItemContent
  titleClass?: string
  descriptionClass?: string
}

const ScrollRevealContentA = ({
  contentA,
  contentB,
  contentC,
  titleClass = defaultTitleClass,
  descriptionClass = defaultDescriptionClass,
  className,
  ...props
}: Props) => {
  const [scrollProgress, setScrollProgress] = React.useState(0)
  const ref0 = useRef(null)

  const { scrollYProgress } = useScroll({
    target: ref0,
  })
  useMotionValueEvent(scrollYProgress, "change", () => {
    // @ts-ignore
    setScrollProgress(scrollYProgress.current)
  })

  return (
    <div className={cn("bg-background", className)} ref={ref0} {...props}>
      <div className="max-w-[90vw] mx-auto">
        <div className="flex w-full mx-auto relative z-20">
          <div
            className={cn(centralColumnStyle, "sticky top-0 flex flex-col w-full items-start justify-center h-[100vh]")}
          >
            <div className="flex flex-row gap-16 md:gap-24 lg:gap-32 xl:gap-40 2xl:gap-48 w-full h-full">
              <div className="lg:!w-[50vw] !w-full h-auto flex flex-col justify-center gap-10 md:gap-auto">
                <PointItem
                  active={true}
                  number="01"
                  title={contentA.title}
                  description={contentA.description}
                  thresholdStart={0}
                  thresholdEnd={0.33}
                  scrollProgress={scrollProgress}
                />
                <PointItem
                  active={true}
                  number="02"
                  title={contentB.title}
                  description={contentB.description}
                  thresholdStart={0.33}
                  thresholdEnd={0.66}
                  scrollProgress={scrollProgress}
                />
                <PointItem
                  active={true}
                  number="03"
                  title={contentC.title}
                  description={contentC.description}
                  thresholdStart={0.66}
                  thresholdEnd={1}
                  scrollProgress={scrollProgress}
                />
              </div>
              <div className="hidden lg:flex flex-col justify-center items-center !w-[50vw] relative h-full">
                <Image
                  width={contentA.image.width}
                  height={contentA.image.height}
                  src={contentA.image.url}
                  alt={contentA.image.alt}
                  className={cn(imageClass, scrollProgress > -1 ? "opacity-100" : "opacity-0")}
                />
                <Image
                  width={contentB.image.width}
                  height={contentB.image.height}
                  src={contentB.image.url}
                  alt={contentB.image.alt}
                  className={cn(imageClass, scrollProgress > 0.33 ? "opacity-100" : "opacity-0")}
                />
                <Image
                  width={contentC.image.width}
                  height={contentC.image.height}
                  src={contentC.image.url}
                  alt={contentC.image.alt}
                  className={cn(imageClass, scrollProgress > 0.66 ? "opacity-100" : "opacity-0")}
                />
              </div>
            </div>
          </div>
          <div className="h-[300vh]" />
        </div>
      </div>
    </div>
  )
}

export default ScrollRevealContentA

const getBarPercentageHeight = (scrollProgress: number, thresholdStart: number, thresholdEnd: number) => {
  if (scrollProgress < thresholdStart) {
    return 0
  }
  if (scrollProgress > thresholdEnd) {
    return 100
  }
  return ((scrollProgress - thresholdStart) / (thresholdEnd - thresholdStart)) * 100
}

const PointItem = ({
  active,
  number,
  title,
  description,
  thresholdStart,
  thresholdEnd,
  scrollProgress,
}: {
  active: boolean
  number: string
  title: string
  description: string
  thresholdStart: number
  thresholdEnd: number
  scrollProgress: number
}) => {
  const barHeightPercentage = getBarPercentageHeight(scrollProgress, thresholdStart, thresholdEnd)
  const isActive = barHeightPercentage > 0
  return (
    <div className={cn("flex flex-col interactive w-full", active ? "opacity-100" : "opacity-50")}>
      <div className="w-full">
        <h3 className={cn(defaultTitleClass, "mb-4 ml-5", isActive ? "opacity-100" : "opacity-50")}>{number}</h3>
      </div>
      <div className="w-full flex relative left-[16px]">
        <div className="w-[70px] flex items-start justify-center relative">
          <div className="h-full w-[2px] bg-foreground/10 absolute top-0 left-[50%] -translate-x-1/2" />
          <div
            className="h-full w-[2px] bg-foreground absolute top-0 left-[50%] -translate-x-1/2"
            style={{ height: `${barHeightPercentage}%` }}
          />
        </div>
        <div className="w-[calc(100% - 40px)] pl-4">
          <div className="flex flex-col gap-1">
            <h3 className={cn(defaultTitleClass, isActive ? "opacity-100" : "opacity-50")}>{title}</h3>
            <p className={cn(defaultDescriptionClass, isActive ? "opacity-100" : "opacity-50")}>{description}</p>
          </div>
        </div>
      </div>
    </div>
  )
}

Installation

npx shadcn@latest add @abui/scroll-reveal-content-a

Usage

import { ScrollRevealContentA } from "@/components/scroll-reveal-content-a"
<ScrollRevealContentA />