Layout 350
standardText Align LeftImage/Video Right2 ColumnsTabsImageMultiple Images

Code
relume-layout350.tsx
"use client";
import React, { useState, useEffect, useRef } from "react";
import { useScroll } from "motion/react";
import { Button, type ButtonProps } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ChevronRight } from "relume-icons";
type ImageProps = {
src: string;
alt?: string;
};
type Section = {
id: string;
anchorTrigger: string;
image: ImageProps;
tagline: string;
heading: string;
description: string;
buttons: ButtonProps[];
};
type Props = {
sections: Section[];
};
export type Layout350Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
export const Layout350 = (props: Layout350Props) => {
const { sections } = {
...Layout350Defaults,
...props,
};
const isProgrammaticScrollRef = useRef(false);
const [activeSection, setActiveSection] = useState(() => {
if (typeof window === "undefined") return sections[0].id;
const hash = window.location.hash.slice(1);
const matchingSection = sections.find((section) => section.id === hash);
return matchingSection?.id ?? sections[0].id;
});
const currentSection = sections.find((section) => section.id === activeSection) ?? sections[0];
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (!element) return;
isProgrammaticScrollRef.current = true;
setActiveSection(sectionId);
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
window.history.pushState(null, "", `#${sectionId}`);
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, 1000);
};
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.slice(1);
if (sections.some((section) => section.id === hash)) {
scrollToSection(hash);
}
};
if (window.location.hash) {
handleHashChange();
}
window.addEventListener("hashchange", handleHashChange);
return () => window.removeEventListener("hashchange", handleHashChange);
}, [sections]);
return (
<section className="px-[5%] py-16 md:py-24 lg:py-28">
<div className="container">
<div className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16 lg:gap-x-20">
<div className="grid items-start gap-12 md:flex md:flex-col md:gap-0">
<Tabs
value={activeSection}
onValueChange={scrollToSection}
className="sticky top-24 z-10 hidden md:block"
>
<TabsList className="flex w-full gap-x-1 rounded-button border border-scheme-border bg-scheme-foreground p-1">
{sections.map((section) => (
<TabsTrigger
key={section.id}
value={section.id}
className="rounded-button px-5 py-2 whitespace-nowrap underline data-[state=active]:bg-scheme-background"
>
{section.anchorTrigger}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{sections.map((section) => (
<ScrollSection
key={section.id}
section={section}
isProgrammaticScrollRef={isProgrammaticScrollRef}
onSectionInView={() => setActiveSection(section.id)}
/>
))}
</div>
<div className="sticky top-0 hidden h-screen flex-col items-center justify-center md:flex">
<div className="w-full">
<img
src={currentSection.image.src}
alt={currentSection.image.alt || "Section image"}
className="w-full rounded-image"
/>
</div>
</div>
</div>
</div>
</section>
);
};
const ScrollSection = ({
section,
isProgrammaticScrollRef,
onSectionInView,
}: {
section: Section;
isProgrammaticScrollRef: React.RefObject<boolean>;
onSectionInView: () => void;
}) => {
const ref = React.useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start center", "end center"],
});
useEffect(() => {
const unsubscribe = scrollYProgress.on("change", (value) => {
if (value > 0.4 && value < 0.6 && !isProgrammaticScrollRef.current) {
onSectionInView();
}
});
return () => unsubscribe();
}, [scrollYProgress, onSectionInView, isProgrammaticScrollRef]);
return (
<div id={section.id} ref={ref} className="flex flex-col items-start justify-center md:h-screen">
<p className="mb-3 font-semibold md:mb-4">{section.tagline}</p>
<h2 className="mb-5 text-h2 font-bold md:mb-6">{section.heading}</h2>
<p className="text-medium">{section.description}</p>
<div className="mt-6 flex items-center gap-x-4 md:mt-8">
{section.buttons.map((button, index) => (
<Button key={index} {...button}>
{button.title}
</Button>
))}
</div>
<div className="mt-10 w-full md:hidden">
<img
src={section.image.src}
alt={section.image.alt || "Section image"}
className="w-full rounded-image"
/>
</div>
</div>
);
};
export const Layout350Defaults = {
sections: [
{
id: "Content-Block-1",
anchorTrigger: "Tab One",
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" } as ButtonProps,
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
} as ButtonProps,
],
},
{
id: "Content-Block-2",
anchorTrigger: "Tab Two",
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" } as ButtonProps,
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
} as ButtonProps,
],
},
{
id: "Content-Block-3",
anchorTrigger: "Tab Three",
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" } as ButtonProps,
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
} as ButtonProps,
],
},
{
id: "Content-Block-4",
anchorTrigger: "Tab Four",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image-4.svg",
alt: "Relume placeholder image 4",
},
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" } as ButtonProps,
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
} as ButtonProps,
],
},
],
};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


