Blocks

Header 144

off_grid
Text Align Left2 ColumnsImage/Video BottomButtonsImage
Header 144

Code

relume-header144.tsx
"use client";

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

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

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

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

export const Header144 = (props: Header144Props) => {
  const { heading, description, buttons, image } = {
    ...Header144Defaults,
    ...props,
  };

  const isMobile = useMediaQuery("(max-width: 767px)");
  const sectionRef = useRef<HTMLElement | null>(null);
  const { scrollYProgress } = useScroll({
    target: sectionRef,
    offset: ["start start", "center 25%"],
  });
  const imageScale = useTransform(scrollYProgress, [0, 1], [0.5, 1]);
  const finalScale = isMobile ? 1 : imageScale;

  return (
    <section ref={sectionRef} className="relative md:min-h-screen">
      <div className="px-[5%] py-16 md:py-24 lg:py-28">
        <div className="container">
          <div className="mb-12 grid grid-cols-1 items-start gap-y-5 md:mb-18 md:grid-cols-2 md:gap-x-12 md:gap-y-8 lg:mb-20 lg:gap-x-20 lg:gap-y-16">
            <h1 className="text-h1 font-bold">{heading}</h1>
            <div className="self-end md:mt-48">
              <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>
          <motion.div
            className="flex origin-top-right flex-col items-end justify-center md:h-[60vh] lg:h-[80vh] lg:justify-start"
            style={{ scale: finalScale }}
          >
            <img src={image.src} alt={image.alt} className="size-full rounded-image object-cover" />
          </motion.div>
        </div>
      </div>
    </section>
  );
};

export const Header144Defaults: 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" }],
  image: {
    src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-landscape.svg",
    alt: "Relume placeholder image",
  },
};

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