Views 221
How I Built a Modern Hero Section with Next.js, Tailwind, and Framer Motion

A good hero section sets the tone for your website. It’s the first thing users see, and it can make them stay or leave within seconds.
Recently, I designed a hero section UI that blends minimalism, motion, and strong typography — all powered by Next.js 15, TailwindCSS, and Framer Motion. In this post, I’ll walk you through how I built it.

Why This Hero Section Works
- Typography Focused: Big, bold headline with a highlight (
through chaos
) to grab attention. - Interactive Visuals: Image cards styled with depth and shadows.
- Motion Powered: Subtle animations make the UI feel alive.
- Clean Setup: Built with Next.js 15 and styled using TailwindCSS for quick customization.
Read the Blog Creating a Minimal Sign-In UI with React and Tailwind CSS
Tools & Dependencies
Here’s what I used:
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@react-three/drei": "^10.7.3",
"@react-three/fiber": "^9.3.0",
"@types/three": "^0.179.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.12",
"lucide-react": "^0.540.0",
"motion": "^12.23.12",
"next": "15.4.7",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"three": "^0.179.1"
}
Code Implementation
- Install this
npx shadcn@latest add https://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,
};
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>
);
}
Using at Hero Section :-
import FlipStack from "@/components/ui/flipstack";
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>
Now Heading Part
MorphText Flip Component
npx shadcn@latest add https://scrollxui.dev/registry/morphotextflip.json
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>
);
}
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>
</>
);
}
Complete working Code 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>
</>
);
}
Final Thoughts
Hero sections are more than decoration – they are storytelling tools. By mixing motion, bold typography, and clean layouts, you can create something that feels professional and inviting.
If you’re building a modern web app, give this hero section a try and adapt it to your brand – follow for more