Building a Stunning Hero Section with Next.js 15, Tailwind CSS, and Framer Motion

A practical guide on building a modern hero section using Next.js 15, TailwindCSS, and Framer Motion. Learn step-by-step how to implement motion, layout, and UI design.
The hero section is the most critical part of a landing page. It’s the first impression, and as developers, we need to balance aesthetics, performance, and usability.
In this article, I’ll show you how I built a modern hero section using:
Next.js 15 (for the app structure)
TailwindCSS (for utility-first styling)
Framer Motion (for smooth animations)
Project Setup
Make sure you’re running Next.js 15 with React 19. Install the required dependencies:
npm install framer-motion tailwind-merge clsx
Optional extras:
@react-three/fiber+threeif you want to experiment with 3D elements.lucide-reactfor icons.
UI Breakdown
Typography Layer – The headline
"Build like it matters through chaos"highlights keywords.Card Layout – Rotated image cards create depth.
Motion Layer – Animations applied via Framer Motion for entry + hover.
Code Implementation
- Install this
npx shadcn@latest addhttps://scrollxui.dev/registry/flipstack.json
After running it will create card component also flipstick aswell
here working code : – Card Component
//File : components/ui/card.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};Copy Code
FlipStick Component
//File : components/ui/flipstack.tsx
"use client";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Card, CardContent } from "@/components/ui/card";
interface FlipStackCard {
id: number;
content?: React.ReactNode;
}
interface FlipStackProps {
cards?: FlipStackCard[];
}
export default function FlipStack({
cards = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}: FlipStackProps) {
const [isInView, setIsInView] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 1024);
checkMobile();
window.addEventListener("resize", checkMobile);
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setIsInView(true);
},
{ threshold: 0.3 }
);
if (containerRef.current) observer.observe(containerRef.current);
return () => {
window.removeEventListener("resize", checkMobile);
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, []);
useEffect(() => {
if (!isMobile || !isInView) return;
const interval = setInterval(() => {
setActiveIndex((prev: number) => (prev + 1) % cards.length);
}, 4000);
return () => clearInterval(interval);
}, [isMobile, isInView, cards.length]);
const getRotation = (index: number) => {
const rotations = [-8, 5, -3, 7, -5, 4, -6, 8, -2, 3];
return rotations[index % rotations.length];
};
const isActive = (index: number) => index === activeIndex;
const getCardVariants = (index: number) => {
const totalCards = cards.length;
const centerIndex = Math.floor(totalCards / 2);
const positionFromCenter = index - centerIndex;
if (isMobile) {
return {
initial: {
opacity: 0,
scale: 0.9,
z: -100,
rotate: getRotation(index),
y: 100,
},
animate: {
opacity: isActive(index) ? 1 : 0.7,
scale: isActive(index) ? 1 : 0.95,
z: isActive(index) ? 0 : -100,
rotate: isActive(index) ? 0 : getRotation(index),
zIndex: isActive(index) ? 40 : totalCards + 2 - index,
y: isActive(index) ? [0, -80, 0] : 0,
},
};
}
return {
initial: {
x: 0,
y: index * 8 + 100,
rotate: getRotation(index),
scale: 1,
zIndex: totalCards - index,
},
animate: {
x: positionFromCenter * 140,
y: Math.abs(positionFromCenter) * 30,
rotate: positionFromCenter * 12,
scale: 1,
zIndex: totalCards - Math.abs(positionFromCenter),
},
};
};
return (
<div className="h-full w-full py-2">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-center items-center">
<div
ref={containerRef}
className="relative h-76 w-[90%] md:max-w-md lg:max-w-md mx-auto"
>
{isMobile ? (
<div className="relative h-full w-full">
<AnimatePresence>
{cards.map((card, index: number) => {
const variants = getCardVariants(index);
return (
<motion.div
key={card.id}
className="absolute inset-0 origin-bottom"
initial="initial"
animate={isInView ? "animate" : "initial"}
exit={{
opacity: 0,
scale: 0.9,
z: 100,
rotate: getRotation(index),
}}
variants={variants}
transition={{ duration: 0.4, ease: "easeInOut" }}
>
<Card
className="w-full h-full border-0 bg-white dark:bg-gray-800 overflow-hidden"
style={{
boxShadow:
" rgba(0, 0, 0, 0.08) 0px 0.839802px 0.503881px -0.3125px, rgba(0, 0, 0, 0.08) 0px 1.99048px 1.19429px -0.625px, rgba(0, 0, 0, 0.08) 0px 3.63084px 2.1785px -0.9375px, rgba(0, 0, 0, 0.08) 0px 6.03627px 3.62176px -1.25px, rgba(0, 0, 0, 0.08) 0px 9.74808px 5.84885px -1.5625px, rgba(0, 0, 0, 0.08) 0px 15.9566px 9.57398px -1.875px, rgba(0, 0, 0, 0.08) 0px 27.4762px 16.4857px -2.1875px, rgba(0, 0, 0, 0.08) 0px 50px 30px -2.5px",
}}
>
<CardContent className="p-0 h-full flex items-center justify-center">
{card.content}
</CardContent>
</Card>
</motion.div>
);
})}
</AnimatePresence>
</div>
) : (
<div
className="relative h-full w-full flex items-center justify-center"
style={{ perspective: "1000px" }}
>
{cards.map((card, index: number) => {
const variants = getCardVariants(index);
return (
<motion.div
key={card.id}
className="absolute origin-bottom"
initial="initial"
animate={isInView ? "animate" : "initial"}
variants={variants}
transition={{
duration: 0.8,
delay: index * 0.1,
ease: "easeOut",
}}
>
<Card
className="w-80 h-66 border-0 bg-white dark:bg-gray-800 overflow-hidden"
style={{
boxShadow:
" rgba(0, 0, 0, 0.08) 0px 0.839802px 0.503881px -0.3125px, rgba(0, 0, 0, 0.08) 0px 1.99048px 1.19429px -0.625px, rgba(0, 0, 0, 0.08) 0px 3.63084px 2.1785px -0.9375px, rgba(0, 0, 0, 0.08) 0px 6.03627px 3.62176px -1.25px, rgba(0, 0, 0, 0.08) 0px 9.74808px 5.84885px -1.5625px, rgba(0, 0, 0, 0.08) 0px 15.9566px 9.57398px -1.875px, rgba(0, 0, 0, 0.08) 0px 27.4762px 16.4857px -2.1875px, rgba(0, 0, 0, 0.08) 0px 50px 30px -2.5px",
}}
>
<CardContent className="p-0 h-full flex items-center justify-center">
{card.content}
</CardContent>
</Card>
</motion.div>
);
})}
</div>
)}
</div>
</div>
</div>
</div>
);
}
Copy Code
Using at Hero Section :-
import FlipStack from "@/components/ui/flipstack";Copy Code
const cards = [
{
id: 1,
content: (
<img
src="https://images.unsplash.com/photo-1611558709798-e009c8fd7706?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Isabelle Carlos"
className="w-full h-full object-cover"
/>
),
},
{
id: 2,
content: (
<img
src="https://plus.unsplash.com/premium_photo-1692340973636-6f2ff926af39?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Lana Akash"
className="w-full h-full object-cover"
/>
),
},
{
id: 3,
content: (
<img
src="https://github.com/Adityakishore0.png?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Ahdeetai"
className="w-full h-full object-cover"
/>
),
},
{
id: 4,
content: (
<img
src="https://images.unsplash.com/photo-1557053910-d9eadeed1c58?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Isabella Mendes"
className="w-full h-full object-cover scale-x-[-1]"
/>
),
},
{
id: 5,
content: (
<img
src="https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Meera Patel"
className="w-full h-full object-cover"
/>
),
},
];
<div className="w-full lg:hidden">
<FlipStack cards={cards} />
</div>
<div className="hidden lg:flex flex-col inset-0 overflow-visible items-center justify-center ">
<FlipStack cards={cards} />
</div>Copy Code
Now Heading Part
MorphText Flip Component
npx shadcn@latest add https://scrollxui.dev/registry/morphotextflip.jsonCopy Code
the prompt will create
//file : components/ui/morphotextflip.tsx
"use client";
import React, { useState, useEffect, useId } from "react";
import { motion, AnimatePresence } from "framer-motion";
const cn = (...classes: (string | undefined | null | false)[]): string =>
classes.filter(Boolean).join(" ");
export interface MorphoTextFlipProps {
words?: string[];
interval?: number;
className?: string;
textClassName?: string;
animationDuration?: number;
animationType?: "slideUp" | "fadeScale" | "flipY" | "slideRotate" | "elastic";
}
export function MorphoTextFlip({
words = ["remarkable", "bold", "scalable", "beautiful"],
interval = 3000,
className,
textClassName,
animationDuration = 700,
animationType = "slideUp",
}: MorphoTextFlipProps) {
const id = useId();
const [currentWordIndex, setCurrentWordIndex] = useState(0);
const [width, setWidth] = useState("auto");
const textRef = React.useRef<HTMLDivElement>(null);
const measureRef = React.useRef<HTMLDivElement>(null);
const updateWidthForWord = () => {
if (measureRef.current) {
const textWidth = measureRef.current.scrollWidth + 48;
setWidth(`${textWidth}px`);
}
};
useEffect(() => {
const timer = setTimeout(() => {
updateWidthForWord();
}, 10);
return () => clearTimeout(timer);
}, [currentWordIndex]);
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentWordIndex((prevIndex) => (prevIndex + 1) % words.length);
}, interval);
return () => clearInterval(intervalId);
}, [words, interval]);
const animationVariants = {
slideUp: {
initial: { y: 40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 },
},
fadeScale: {
initial: { scale: 0.8, opacity: 0 },
animate: { scale: 1, opacity: 1 },
exit: { scale: 1.2, opacity: 0 },
},
flipY: {
initial: { rotateY: 90, opacity: 0 },
animate: { rotateY: 0, opacity: 1 },
exit: { rotateY: -90, opacity: 0 },
},
slideRotate: {
initial: { x: 100, rotate: 10, opacity: 0 },
animate: { x: 0, rotate: 0, opacity: 1 },
exit: { x: -100, rotate: -10, opacity: 0 },
},
elastic: {
initial: { scale: 0, rotate: -180 },
animate: { scale: 1, rotate: 0 },
exit: { scale: 0, rotate: 180 },
},
};
const currentVariant = animationVariants[animationType];
const duration = animationDuration / 1000;
return (
<motion.div
layout
layoutId={`words-container-${id}`}
animate={{ width }}
transition={{
duration: duration * 0.4,
ease: "easeInOut",
type: "spring",
stiffness: 300,
damping: 30,
}}
className={cn(
"relative inline-block overflow-hidden rounded-2xl px-6 pt-2 pb-3",
"backdrop-blur-sm border border-gray-200 shadow-xl",
"bg-white/70 dark:bg-slate-800/70",
"dark:border-slate-700",
className
)}
>
<div className="relative flex items-center justify-center">
<div
ref={measureRef}
className={cn(
"absolute opacity-0 pointer-events-none whitespace-nowrap",
"text-4xl font-bold md:text-7xl",
textClassName
)}
style={{ top: -9999 }}
>
{words[currentWordIndex]}
</div>
<AnimatePresence mode="wait">
<motion.div
key={words[currentWordIndex]}
initial={currentVariant.initial}
animate={currentVariant.animate}
exit={currentVariant.exit}
transition={{
duration: duration * 0.6,
ease:
animationType === "elastic"
? [0.68, -0.55, 0.265, 1.55]
: "easeInOut",
}}
className={cn(
"text-4xl font-bold text-rose-600 dark:text-rose-400 md:text-7xl whitespace-nowrap",
textClassName
)}
ref={textRef}
>
{words[currentWordIndex]}
</motion.div>
</AnimatePresence>
</div>
</motion.div>
);
}Copy Code
a sub component for main heading text and using morphtext aswell
import { MorphoTextFlip } from "./morphotextflip";
export default function HeroSection() {
return (
<>
<section className="flex flex-col items-center justify-center px-4">
<h1 className="text-4xl md:text-7xl font-bold text-center mb-4">
Build like it matters
</h1>
<MorphoTextFlip
words={["with heart", "through chaos", "for impact", "beyond limits"]}
textClassName="text-4xl md:text-7xl text-rose-600 dark:text-rose-400 font-bold mt-1"
animationType="slideUp"
/>
</section>
</>
);
}
Copy Code
Complete working code for HeroSection
import FlipStack from "@/components/ui/flipstack";
import HeroSection from "@/components/ui/hero";
export default function FlipStickPage() {
const cards = [
{
id: 1,
content: (
<img
src="https://images.unsplash.com/photo-1611558709798-e009c8fd7706?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Isabelle Carlos"
className="w-full h-full object-cover"
/>
),
},
{
id: 2,
content: (
<img
src="https://plus.unsplash.com/premium_photo-1692340973636-6f2ff926af39?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Lana Akash"
className="w-full h-full object-cover"
/>
),
},
{
id: 3,
content: (
<img
src="https://github.com/Adityakishore0.png?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Ahdeetai"
className="w-full h-full object-cover"
/>
),
},
{
id: 4,
content: (
<img
src="https://images.unsplash.com/photo-1557053910-d9eadeed1c58?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Isabella Mendes"
className="w-full h-full object-cover scale-x-[-1]"
/>
),
},
{
id: 5,
content: (
<img
src="https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3"
alt="Meera Patel"
className="w-full h-full object-cover"
/>
),
},
];
return (
<>
<section className="w-full h-screen bg-[#F5F5F5] flex flex-col items-center justify-center">
<div className="mb-4">
<HeroSection />
</div>
<div className="w-full lg:hidden">
<FlipStack cards={cards} />
</div>
<div className="hidden lg:flex flex-col inset-0 overflow-visible items-center justify-center ">
<FlipStack cards={cards} />
</div>
<button
className="w-[240px] h-[58px] flex justify-center items-center gap-2 px-4 py-2 bg-black text-white cursor-pointer rounded-2xl mt-8 transition hover:bg-gradient-to-r hover:from-black hover:to-gray-800
hover:scale-105 active:scale-95"
style={{
boxShadow:
" rgba(255, 255, 255, 0.15) 0px 0px 20px 1.64px inset, rgba(0, 0, 0, 0.13) 0px 0.839802px 0.503881px -0.3125px, rgba(0, 0, 0, 0.13) 0px 1.99048px 1.19429px -0.625px, rgba(0, 0, 0, 0.13) 0px 3.63084px 2.1785px -0.9375px, rgba(0, 0, 0, 0.13) 0px 6.03627px 3.62176px -1.25px, rgba(0, 0, 0, 0.13) 0px 9.74808px 5.84885px -1.5625px, rgba(0, 0, 0, 0.13) 0px 15.9566px 9.57398px -1.875px, rgba(0, 0, 0, 0.13) 0px 27.4762px 16.4857px -2.1875px, rgba(0, 0, 0, 0.13) 0px 50px 30px -2.5px",
}}
>
Get Started
<span className="rotate-[90deg]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18"
/>
</svg>
</span>
</button>
</section>
</>
);
}
Copy Code
Takeaways
A hero section doesn’t need to be complex — focus on typography, motion, and structure.
Framer Motion and Tailwind CSS make it simple to experiment with designs.
This approach scales well for landing pages, portfolios, and SaaS apps.
Follow for more - webcomponents mainnet


