Layout 348
standardText Align Left2 ColumnsImage/Video LeftButtonsImageMultiple Images

Code
relume-layout348.tsx
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import clsx from "clsx";
import { Button, type ButtonProps } from "@/components/ui/button";
import { ChevronRight } from "relume-icons";
type ImageProps = {
src: string;
alt?: string;
};
type ContentProps = {
tagline: string;
heading: string;
description: string;
buttons: ButtonProps[];
image: ImageProps;
};
type Props = {
contents: ContentProps[];
images: ImageProps[];
};
export type Layout348Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
export const Layout348 = (props: Layout348Props) => {
const { contents, images } = {
...Layout348Defaults,
...props,
};
const [activeSection, setActiveSection] = useState(0);
const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
const overlayIndices = useMemo(() => new Set([0, 2]), []);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) {
const idx = Number((visible.target as HTMLElement).dataset.index);
if (!Number.isNaN(idx)) setActiveSection((prev) => (prev === idx ? prev : idx));
}
},
{ threshold: [0.5, 0.75, 1] },
);
sectionRefs.current.forEach((el: HTMLDivElement | null) => el && observer.observe(el));
return () => observer.disconnect();
}, []);
return (
<section className="relative isolate">
<div className="pointer-events-none absolute inset-0 -z-10">
<div
className={clsx(
"h-full bg-scheme-foreground transition-opacity duration-300 md:sticky md:top-0 md:h-screen",
overlayIndices.has(activeSection) ? "opacity-100" : "opacity-0",
)}
/>
</div>
<div className="px-[5%]">
<div className="container">
<div className="relative grid gap-x-12 py-16 sm:gap-y-12 md:grid-cols-2 md:py-0 lg:gap-x-20">
<div className="sticky top-0 hidden h-screen md:flex md:flex-col md:items-center md:justify-center">
{images.map((image, index) => (
<img
key={index}
src={image.src}
className={clsx("absolute w-full rounded-image", {
"opacity-100": activeSection === index,
"opacity-0": activeSection !== index,
})}
alt={image.alt}
/>
))}
</div>
<div className="grid grid-cols-1 gap-12 md:block">
{contents.map((content, index) => (
<div
key={index}
ref={(el) => {
sectionRefs.current[index] = el;
}}
data-index={index}
>
<div className="flex flex-col items-start justify-center md:h-screen">
<p className="mb-3 font-semibold md:mb-4">{content.tagline}</p>
<h2 className="mb-5 text-h2 font-bold md:mb-6">{content.heading}</h2>
<p className="text-medium">{content.description}</p>
<div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8">
{content.buttons.map((button, index) => (
<Button key={index} {...button}>
{button.title}
</Button>
))}
</div>
<div className="mt-10 block w-full md:hidden">
<img
src={content.image.src}
className="w-full rounded-image"
alt={content.image.alt}
/>
</div>
{null}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
export const Layout348Defaults: Props = {
contents: [
{
tagline: "Tagline",
heading: "Medium length section 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", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
alt: "Relume placeholder image 1",
},
},
{
tagline: "Tagline",
heading: "Medium length section 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", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
alt: "Relume placeholder image 2",
},
},
{
tagline: "Tagline",
heading: "Medium length section 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", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
alt: "Relume placeholder image 3",
},
},
{
tagline: "Tagline",
heading: "Medium length section 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", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
alt: "Relume placeholder image 4",
},
},
],
images: [
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-1.svg",
alt: "Relume placeholder image 1",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-2.svg",
alt: "Relume placeholder image 2",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-3.svg",
alt: "Relume placeholder image 3",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
alt: "Relume placeholder image 4",
},
],
};npm i @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tabs class-variance-authority@0.7.1 clsx@^2.1.1 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2


