Portfolio 21
off_grid1 ColumnText Align CenterTagsMultiple ImagesOverlapping ImagesImage

Code
relume-portfolio21.tsx
import { useRef } from "react";
import { motion, useScroll, useTransform, useSpring } from "motion/react";
import clsx from "clsx";
import { Badge } from "@/components/ui/badge";
type ImageProps = {
src: string;
alt?: string;
};
type Project = {
heading: string;
imageCenter: ImageProps;
imageLeft: ImageProps;
imageRight: ImageProps;
tags: string[];
};
type Props = {
projects: Project[];
};
export type Portfolio21Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
export const Portfolio21 = (props: Portfolio21Props) => {
const { projects } = {
...Portfolio21Defaults,
...props,
};
const transformRef = useRef(null);
const { scrollYProgress } = useScroll({
target: transformRef,
offset: ["start start", "end end"],
});
const smoothProgress = useSpring(scrollYProgress, {
stiffness: 200,
damping: 25,
restDelta: 0.001,
mass: 0.5,
});
const computedStyles = projects.map((_, index) => {
const blockOffset = projects.length ? index / projects.length : 0;
return {
headerStyles: {
opacity: useTransform(
smoothProgress,
[0.0045 + blockOffset, 0.022 + blockOffset, 0.06 + blockOffset, 0.082 + blockOffset],
[0, 1, 1, 0],
),
scale: useTransform(
smoothProgress,
[0 + blockOffset, 0.022 + blockOffset, 0.06 + blockOffset, 0.082 + blockOffset],
[0.9, 1, 1, 0.9],
),
},
imageLeftStyles: {
y: useTransform(
smoothProgress,
[0.17 + blockOffset, 0.25 + blockOffset],
["0vh", "-200vh"],
),
},
imageRightStyles: {
y: useTransform(
smoothProgress,
[0.22 + blockOffset, 0.26 + blockOffset],
["0vh", "-200vh"],
),
},
};
});
return (
<section className="relative px-[5%]">
<div ref={transformRef} className="container">
<div className="py-12 md:py-16 lg:py-20">
{projects.map((project, index) => {
return (
<div
key={index}
className={clsx("relative flex h-[500vh] flex-col", {
"mt-[-40vh]": index > 0,
})}
>
<motion.div
className="sticky top-0 flex h-[150vh] justify-center"
style={computedStyles[index].headerStyles}
>
<div className="absolute top-[50vh] -z-10 flex w-full max-w-lg flex-col items-center text-center">
<h3 className="text-h2 font-bold">{project.heading}</h3>
<div className="mt-4 flex flex-wrap justify-center gap-2">
{project.tags.map((tag, index) => (
<Badge key={index}>{tag}</Badge>
))}
</div>
</div>
</motion.div>
<div className="sticky top-0 z-10 flex h-screen items-center justify-center overflow-hidden">
<a href="#" className="inline-block max-w-full">
<div className="flex h-screen w-full max-w-lg items-center justify-center overflow-hidden">
<img
src={project.imageCenter.src}
alt={project.imageCenter.alt}
className="w-full rounded-image"
/>
</div>
</a>
<div className="absolute inset-y-0 right-auto left-0 z-20 hidden h-screen w-full max-w-xxs lg:flex">
<motion.div
className="relative w-full pt-[100vh]"
style={computedStyles[index].imageLeftStyles}
>
<img
src={project.imageLeft.src}
alt={project.imageLeft.alt}
className="w-full rounded-image"
/>
</motion.div>
</div>
<div className="absolute inset-y-0 right-0 left-auto z-20 hidden h-screen w-full max-w-xxs lg:flex">
<motion.div
className="relative w-full pt-[100vh]"
style={computedStyles[index].imageRightStyles}
>
<img
src={project.imageRight.src}
alt={project.imageRight.alt}
className="w-full rounded-image"
/>
</motion.div>
</div>
</div>
<div className="absolute inset-0 -z-10 mt-[100vh]" />
</div>
);
})}
</div>
</div>
</section>
);
};
export const Portfolio21Defaults: Props = {
projects: [
{
heading: "Project name",
tags: ["Tag one", "Tag two", "Tag three"],
imageCenter: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image center",
},
imageLeft: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image left",
},
imageRight: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image right",
},
},
{
heading: "Project name",
tags: ["Tag one", "Tag two", "Tag three"],
imageCenter: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image center",
},
imageLeft: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image left",
},
imageRight: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image right",
},
},
{
heading: "Project name",
tags: ["Tag one", "Tag two", "Tag three"],
imageCenter: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image center",
},
imageLeft: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image left",
},
imageRight: {
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image right",
},
},
],
};npm i @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


