Blocks

Header 156

Hero
Text Align LeftImage/Video BottomButtonsMultiple Images
Header 156

Code

relume-header156.tsx
import { Button, type ButtonProps } from "@/components/ui/button";
import { useMediaQuery } from "@/hooks/use-media-query";
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";

type ImageProps = {
  src: string;
  alt?: string;
};

type Props = {
  heading: string;
  description: string;
  buttons: ButtonProps[];
  images: ImageProps[];
};

export type Header156Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;

export const Header156 = (props: Header156Props) => {
  const { heading, description, buttons, images } = {
    ...Header156Defaults,
    ...props,
  };

  const containerRef = useRef<HTMLElement>(null);
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["start start", "end end"],
  });

  const isMobile = useMediaQuery("(max-width: 767px)");
  const isTablet = useMediaQuery("(min-width: 768px) and (max-width: 991px)");

  const desktopOffsets = [
    { x: "-20%", y: "-20%", scale: 0.8 },
    { x: "0%", y: "110%", scale: 0.8 },
    { x: "0%", y: "-60%", scale: 0.8 },
    { x: "20%", y: "50%", scale: 0.8 },
  ];

  const mobileOffsets = [
    { x: "0%", y: "-10vh", scale: 0.8 },
    { x: "10%", y: "-15vh", scale: 0.8 },
    { x: "-10%", y: "20vh", scale: 0.8 },
    { x: "0%", y: "15vh", scale: 0.8 },
  ];

  const tabletOffsets = [
    { x: "0%", y: "-30%", scale: 0.8 },
    { x: "0%", y: "140%", scale: 0.8 },
    { x: "0%", y: "-60%", scale: 0.8 },
    { x: "0%", y: "118%", scale: 0.8 },
  ];

  const imageOffsets = isMobile ? mobileOffsets : isTablet ? tabletOffsets : desktopOffsets;

  const transforms = imageOffsets.map((offset) => ({
    x: useTransform(scrollYProgress, [0, 1], ["0%", offset.x]),
    y: useTransform(scrollYProgress, [0, 1], ["0%", offset.y]),
    scale: useTransform(scrollYProgress, [0, 1], [1, offset.scale]),
  }));

  const imageTransforms = imageOffsets.map(() => useTransform(scrollYProgress, [0, 1], [1.2, 1]));

  const descriptionOpacity = useTransform(scrollYProgress, [0.9, 1], [0, 1]);

  return (
    <section ref={containerRef} className="relative min-h-[180vh] lg:mb-28">
      <div className="px-[5%] pt-16 md:pt-24 lg:pt-28">
        <div className="container">
          <div className="grid grid-cols-1 items-start gap-x-12 gap-y-5 md:grid-cols-2 md:gap-y-8 lg:gap-x-20 lg:gap-y-16">
            <h1 className="text-h1 font-bold">{heading}</h1>
            <div>
              <p className="text-medium">{description}</p>
              <div className="mt-6 flex flex-wrap gap-4 md:mt-8">
                {buttons.map((button, index) => (
                  <Button key={index} {...button}>
                    {button.title}
                  </Button>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>

      <div className="sticky top-0 flex items-start justify-center px-[5%] pt-12 pb-16 md:h-screen md:py-20">
        <div className="container">
          <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
            {images.map((image, index) => (
              <motion.div
                key={index}
                className="w-full overflow-hidden rounded-image"
                style={{
                  x: transforms[index]?.x ?? "0%",
                  y: transforms[index]?.y ?? "0%",
                  scale: transforms[index]?.scale ?? 1,
                }}
              >
                <motion.img
                  src={image.src}
                  className="aspect-2/3 size-full object-cover md:aspect-3/4"
                  style={{ scale: imageTransforms[index] ?? 1 }}
                  alt={image.alt}
                />
              </motion.div>
            ))}
          </div>
        </div>
        <motion.div
          style={{
            opacity: descriptionOpacity,
          }}
          className="absolute self-center text-center"
        >
          <div className="px-[5%]">
            <div className="mx-auto w-full max-w-md">
              <p className="text-medium">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in
                eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum
                nulla, ut commodo diam libero vitae erat.
              </p>
            </div>
          </div>
        </motion.div>
      </div>
    </section>
  );
};

export const Header156Defaults: Props = {
  heading: "Medium length hero heading goes here",
  description:
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
  buttons: [{ title: "Button" }, { title: "Button", variant: "secondary" }],
  images: [
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
      alt: "Relume placeholder image 1",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
      alt: "Relume placeholder image 2",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
      alt: "Relume placeholder image 3",
    },
    {
      src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
      alt: "Relume placeholder image 4",
    },
  ],
};

npm i @radix-ui/react-accordion @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tabs class-variance-authority@0.7.1 clsx@^2.1.1 embla-carousel-react@^8.5.2 framer-motion motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2