A React services section with animated marquee text. Built with Motion and Tailwind CSS for landing pages.
import { ServicesSection } from "@/components/services";
import { MotionLenisProvider } from "@/providers/motion-lenis-provider";
import { NoisyBackground } from "@/components/noisy-background";
const ServicesDemo = () => {
return (
<MotionLenisProvider className="relative size-full">
<NoisyBackground
className="z-20 opacity-50"
patternUrl="/assets/noisy-background/noisy_texture_2.webp"
/>
<div className="h-svh">section one</div>
<ServicesSection />
<div className="h-svh">section thrww</div>
</MotionLenisProvider>
)
}The demo is using the NoisyBackground component with a custom pattern bg image you can download by clicking here (direct download)
The components uses the Markee component to create the marquee effect.
Install the following dependencies:
npm install motion lucide-react react-use-measurebun add motion lucide-react react-use-measuredeno add motion lucide-react react-use-measurepnpm add motion lucide-react react-use-measureyarn add motion lucide-react react-use-measureCopy and paste the following code into your project.
"use client";
import {
AnimatePresence,
MotionConfig,
motion,
type Transition,
useReducedMotion,
type Variants,
} from "motion/react";
import { Montserrat } from "next/font/google";
import { Fragment, useState } from "react";
import { Markee, MarkeeContent, MarkeeItem } from "@/components/markee";
import { cn } from "@/lib/utils";
const fontSerrat = Montserrat({
subsets: ["latin"],
variable: "--font-serrat",
weight: ["400", "500", "600", "700", "800", "900"],
});
const MARQUEE_REPEAT = 8;
const items = [
{ href: "#about", title: "About" },
{ href: "#projects", title: "Projects" },
{ href: "#work", title: "Work" },
{ href: "#contact", title: "Contact" },
] as const;
const listVariants: Variants = {
hidden: {},
visible: {
transition: {
delayChildren: 0.05,
staggerChildren: 0.08,
},
},
};
const itemSlotVariants: Variants = {
hidden: {},
visible: {},
};
const titleReveal: Variants = {
hidden: { y: "110%" },
visible: {
transition: { bounce: 0.1, duration: 1.5, type: "spring" },
y: "0%",
},
};
const baseTransition: Transition = { bounce: 0, duration: 0.6, type: "spring" };
const titleRevealReduced: Variants = {
hidden: { y: "0%" },
visible: { transition: { duration: 0 }, y: "0%" },
};
function ServiceMenuItem({
title,
href,
shouldReduceMotion,
}: {
title: string;
href: string;
shouldReduceMotion: boolean;
}) {
const [isActive, setIsActive] = useState(false);
const marqueeLabel = `[ VIEW ${title.toUpperCase()} ]`;
return (
<motion.li
className="relative w-full text-center"
variants={itemSlotVariants}
>
<a
className="relative flex w-full items-center justify-center outline-none focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-8"
href={href}
onBlur={() => setIsActive(false)}
onFocus={() => setIsActive(true)}
onMouseEnter={() => setIsActive(true)}
onMouseLeave={() => setIsActive(false)}
>
<motion.div
animate={
isActive
? shouldReduceMotion
? { opacity: 0.45 }
: { filter: "blur(10px)", opacity: 0.45, scale: 0.99 }
: shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1, scale: 1 }
}
className="flex w-full justify-center"
initial={false}
transition={{
bounce: 0,
duration: 0.6,
type: "spring",
}}
>
<span aria-hidden="true" className="block overflow-hidden">
<motion.span
className="inline-block font-extrabold text-[#e4e4e4] text-[clamp(3.75rem,12vw,9rem)] uppercase leading-[0.85] tracking-normal"
style={{ transformOrigin: "center" }}
variants={shouldReduceMotion ? titleRevealReduced : titleReveal}
>
{title}
</motion.span>
</span>
<span className="sr-only">{title}</span>
</motion.div>
<AnimatePresence>
{isActive ? (
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1, scale: 1 }
}
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-1/2 z-10 w-full -translate-y-1/2"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0.15 } }
: {
filter: "blur(4px)",
opacity: 0,
scale: 0.95,
}
}
initial={
shouldReduceMotion
? { opacity: 0 }
: { filter: "blur(4px)", opacity: 0, scale: 0.95 }
}
transition={baseTransition}
>
<Markee
aria-hidden="true"
aria-live="off"
className="w-full max-w-none"
>
<MarkeeContent duration={14} ease="linear">
{Array.from({ length: MARQUEE_REPEAT }, (_, index) => (
<Fragment key={`${title}-marquee-${index}`}>
<MarkeeItem className="shrink-0 px-6 font-medium text-[#e4e4e4] text-sm uppercase tracking-wide md:text-base">
{marqueeLabel}
</MarkeeItem>
</Fragment>
))}
</MarkeeContent>
</Markee>
</motion.div>
) : null}
</AnimatePresence>
</a>
</motion.li>
);
}
const ServicesSection: React.FC = () => {
const shouldReduceMotion = useReducedMotion() ?? false;
return (
<MotionConfig reducedMotion="user">
<nav
aria-label="Services Section"
aria-labelledby="page-sections-heading"
className={cn(
"relative min-h-svh w-full overflow-hidden bg-[#101010]",
fontSerrat.className
)}
>
<motion.ul
className="relative z-30 flex min-h-svh w-full flex-col items-center justify-center"
initial="hidden"
role="list"
variants={shouldReduceMotion ? undefined : listVariants}
viewport={{ amount: 0.4 }}
whileInView="visible"
>
{items.map((item) => (
<ServiceMenuItem
href={item.href}
key={item.title}
shouldReduceMotion={shouldReduceMotion}
title={item.title}
/>
))}
</motion.ul>
</nav>
</MotionConfig>
);
};
export { ServicesSection };