A rounded photo banner whose image drifts on scroll, with a centred serif heading and button. React and Tailwind CSS, no dependencies.
ParallaxImageCTA.tsx
"use client";
// Rounded photo banner whose image drifts as you scroll (parallax), with a
// centred serif heading and button. Pure Tailwind + React, no dependencies. Works with Tailwind v3 and v4.
import { useEffect, useRef } from "react";
type Props = {
heading?: string;
cta?: { label: string; href: string };
image?: string;
imageAlt?: string;
strength?: number;
};
export default function ParallaxImageCta({
heading = "Escape to stillness, privacy and soul",
cta = { label: "Find yourself a room", href: "#" },
image = "https://images.unsplash.com/photo-1784558525815-87e21a67b3f3?w=2400&q=80&auto=format&fit=crop",
imageAlt = "Infinity pool at sunset",
strength = 80,
}: Props) {
const box = useRef<HTMLDivElement>(null);
const photo = useRef<HTMLImageElement>(null);
useEffect(() => {
const el = box.current;
const pic = photo.current;
if (!el || !pic || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let frame = 0;
const update = () => {
const rect = el.getBoundingClientRect();
// -1 when the banner is below the viewport, 1 when it has scrolled past
const progress = Math.max(-1, Math.min(1, (window.innerHeight / 2 - (rect.top + rect.height / 2)) / window.innerHeight));
pic.style.transform = `translate3d(0, ${progress * strength}px, 0) scale(1.2)`;
};
const onScroll = () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
cancelAnimationFrame(frame);
};
}, [strength]);
return (
<section className="bg-white px-5 py-16 font-['Inter',ui-sans-serif,system-ui,sans-serif] antialiased md:px-10">
<div ref={box} className="relative mx-auto h-[70vh] min-h-[480px] max-w-7xl overflow-hidden rounded-2xl bg-neutral-900">
<img ref={photo} src={image} alt={imageAlt} className="absolute inset-0 h-full w-full object-cover will-change-transform" style={{ transform: "scale(1.2)" }} />
<div className="absolute inset-0 bg-black/25" />
<div className="absolute inset-0 z-10 flex flex-col items-center justify-end gap-8 px-6 py-20 text-center md:py-28">
<h2 className="max-w-2xl font-['Playfair_Display',Georgia,serif] text-[clamp(2.25rem,5vw,4.5rem)] leading-[1.1] text-white">{heading}</h2>
<a href={cta.href} className="rounded-full bg-[#99023C] px-8 py-4 text-sm font-semibold text-white transition-colors hover:bg-[#C9034F]">
{cta.label}
</a>
</div>
</div>
</section>
);
}
How to use it
- Make sure your Next.js or React project has Tailwind CSS set up (v3 or v4 both work).
- Create
components/ParallaxImageCTA.tsxand paste the code. - Import it into a page and replace the sample text and images with your own.
