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.

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

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.
How I Built a Modern Hero Section with Next.js, Tailwind, and Framer Motion

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

  1. 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

Share your love
The Fallen
The Fallen
Articles: 72

Leave a Reply

Your email address will not be published. Required fields are marked *

hii
index
0%

How did you reach us?