Layout 352
standardText Align LeftText Align Center3 ColumnsIconsButtonsMultiple Images

Code
relume-layout352.tsx
"use client";
import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";
import { Button, type ButtonProps } from "@/components/ui/button";
import { ChevronRight } from "relume-icons";
type ImageProps = {
src: string;
alt: string;
};
type Content = {
tagline: string;
heading: string;
description: string;
buttons: ButtonProps[];
};
type TimelineItem = {
date: string;
description: string;
image: ImageProps;
timelineButtons: ButtonProps[];
};
type Props = {
featureContent: Content;
timelineItems: TimelineItem[];
};
export type Layout352Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
export const Layout352 = (props: Layout352Props) => {
const { featureContent, timelineItems } = {
...Layout352Defaults,
...props,
};
const timelineRef = useRef<HTMLDivElement>(null);
const { scrollYProgress: lineProgress } = useScroll({
target: timelineRef,
offset: ["start center", "end center"],
});
const lineHeight = useTransform(lineProgress, [0, 1], ["0%", "100%"]);
return (
<section className="relative z-0 overflow-clip">
<Content {...featureContent} />
<div className="px-[5%]">
<div className="container">
<div className="relative flex flex-col items-center justify-center">
<div
ref={timelineRef}
className="absolute left-1.5 -z-20 h-full w-[3px] bg-scheme-text/20 md:left-auto"
>
<motion.div
className="absolute top-0 right-0 left-0 w-[3px] bg-scheme-text"
style={{ height: lineHeight }}
/>
<div className="absolute top-0 right-0 left-0 z-10 h-24 w-full bg-gradient-to-b from-white to-transparent" />
<div className="absolute right-0 bottom-0 left-0 z-10 h-24 w-full bg-gradient-to-t from-white to-transparent" />
</div>
{timelineItems.map((item, index) => (
<TimelineItem key={index} {...item} />
))}
</div>
</div>
</div>
<Content {...featureContent} />
</section>
);
};
const TimelineItem = ({ date, description, image, timelineButtons }: TimelineItem) => {
const circleRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: circleRef,
offset: ["end end", "end center"],
});
const circleOpacity = useTransform(scrollYProgress, [0.85, 1], [0, 1]);
const contentOpacity = useTransform(scrollYProgress, [0, 0.5, 0.85, 1], [0, 0.25, 0.75, 1]);
return (
<div className="relative z-20 grid w-full auto-cols-fr grid-cols-[3rem_1fr] gap-y-6 py-16 sm:grid-cols-[4rem_1fr] md:w-auto md:grid-cols-[1fr_10rem_1fr] md:gap-y-0 lg:grid-cols-[1fr_12rem_1fr]">
<motion.div
className="[grid-area:1/2/2/3] md:text-right md:[grid-area:auto]"
style={{ opacity: contentOpacity }}
>
<h3 className="text-h3 font-bold">{date}</h3>
</motion.div>
<div className="flex justify-start [grid-area:1/1/3/2] md:justify-center md:[grid-area:auto]">
<div
ref={circleRef}
className="sticky top-[50vh] size-3.75 rounded-full shadow-[0_0_0_8px_var(--color-scheme-background)] backdrop-blur-3xl"
>
<div className="absolute inset-0 rounded-full bg-scheme-text/15" />
<motion.div
className="absolute inset-0 rounded-full bg-scheme-text"
style={{ opacity: circleOpacity }}
/>
</div>
</div>
<motion.div style={{ opacity: contentOpacity }}>
<div className="mb-10 md:mb-14 lg:mb-16">
<p className="text-medium">{description}</p>
<div className="mt-6 flex flex-wrap items-center gap-4 md:mt-8">
{timelineButtons.map((button, index) => (
<Button key={index} {...button}>
{button.title}
</Button>
))}
</div>
</div>
<div className="overflow-hidden">
<img src={image.src} alt={image.alt} className="rounded-image" />
</div>
</motion.div>
</div>
);
};
const Content = ({ ...content }: Content) => (
<div className="px-[5%] py-16 md:py-24 lg:py-28">
<div className="container">
<div className="mx-auto max-w-lg text-center">
<p className="mb-3 font-semibold md:mb-4">{content.tagline}</p>
<h1 className="mb-5 text-h2 font-bold md:mb-6">{content.heading}</h1>
<p className="text-medium">{content.description}</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-4 md:mt-8">
{content.buttons.map((button, index) => (
<Button key={index} {...button}>
{button.title}
</Button>
))}
</div>
</div>
</div>
</div>
);
export const Layout352Defaults: Props = {
featureContent: {
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" />,
},
],
},
timelineItems: [
{
date: "Date",
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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 1",
},
timelineButtons: [
{ title: "Button", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
},
{
date: "Date",
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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 2",
},
timelineButtons: [
{ title: "Button", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
},
{
date: "Date",
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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 3",
},
timelineButtons: [
{ title: "Button", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
},
{
date: "Date",
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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 4",
},
timelineButtons: [
{ title: "Button", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
},
{
date: "Date",
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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
image: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 4",
},
timelineButtons: [
{ title: "Button", variant: "secondary" },
{
title: "Button",
variant: "link",
size: "link",
iconRight: <ChevronRight className="text-scheme-text" />,
},
],
},
],
};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


