ComponentsChangelogPro

    Getting Started

    Animated Cards

    Components

    Backgrounds

    3d & Shaders

    Text Animations

    Buttons

    Docs
    Showcase

    Showcase

    The Showcase component is an engaging and interactive way to present and highlight your application. Equipped with immersive 3D effects, smooth animations, and customizable layers, it brings your content to life.

    Loading...

    Installation

    Install dependencies

    npm install

    npm install clsx tailwind-merge framer-motion

    Add util file

    lib/utils.ts

    import { ClassValue, clsx } from "clsx";
    import { twMerge } from "tailwind-merge";
     
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs));
    }

    Copy the showcase-example file (optional)

    The showcase-example file contains the body and header of the showcase. It's the example used to build this component. It's optional but can be helpful as a starting point to build your own.

    showcase-example.tsx

    "use client";
     
    import { useRef, useState, useEffect, ReactNode } from "react";
    import React from "react";
    import { cn } from "@/lib/utils";
    import {
      motion,
      useMotionTemplate,
      useMotionValue,
      useSpring,
    } from "framer-motion";
     
    interface ShowcaseSpotlightProps {
      children: ReactNode;
      className?: string;
    }
     
    export function ShowcaseSpotlight({ children, className }: ShowcaseSpotlightProps) {
      return (
        <div
          className={cn(
            "w-full h-full rounded-xl outline outline-transparent outline-1 outline-offset-0 dark:hover:outline-zinc-700 hover:outline-zinc-100 hover:outline-offset-4 transition-all scale-100 hover:scale-[103%]",
            className
          )}
        >
          {children}
        </div>
      );
    }
     
    interface ShowcaseHeaderProps {
      children: ReactNode;
    }
     
    export function ShowcaseHeader({ children }: ShowcaseHeaderProps) {
      return (
        <motion.div
          initial={{ opacity: 0, scale: 0.98 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={{ delay: 0.1, duration: 1, ease: "easeOut" }}
          className="relative w-full flex items-start h-full"
        >
          <div className="h-14 px-4 w-full flex items-center">{children}</div>
        </motion.div>
      );
    }
     
    interface ShowcaseBodyProps {
      children: ReactNode;
    }
     
    export function ShowcaseBody({ children }: ShowcaseBodyProps) {
      const [isMedium, setIsMedium] = useState(false);
     
      useEffect(() => {
        const handleResize = () => {
          setIsMedium(window.innerWidth >= 768);
        };
     
        handleResize();
        window.addEventListener("resize", handleResize);
     
        return () => {
          window.removeEventListener("resize", handleResize);
        };
      }, []);
     
      return (
        <div
          style={{
            transform: isMedium ? "translateZ(75px)" : "translateZ(0px)",
            transformStyle: "preserve-3d",
          }}
          className="absolute z-20 inset-0 top-14 flex flex-col md:justify-end overflow-hidden"
        >
          <motion.div
            initial={{ opacity: 0, scale: 0.98 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ delay: 0.2, duration: 1, ease: "easeOut" }}
            className="h-full px-4 relative w-full flex"
          >
            {children}
          </motion.div>
        </div>
      );
    }
     
    interface ShowcaseProps {
      children: ReactNode;
      damping?: number;
      swiftness?: number;
      maxRotation?: number;
    }
     
    export function Showcase({
      children,
      damping = 20,
      swiftness = 80,
      maxRotation = 25,
    }: ShowcaseProps) {
      const [isMedium, setIsMedium] = useState(false);
     
      useEffect(() => {
        const handleResize = () => {
          setIsMedium(window.innerWidth >= 768);
        };
     
        handleResize();
        window.addEventListener("resize", handleResize);
     
        return () => {
          window.removeEventListener("resize", handleResize);
        };
      }, []);
     
      const halfMaxRotation = maxRotation / 2;
     
      const refParentDiv = useRef<HTMLDivElement>(null);
      const refMotionDiv = useRef<HTMLDivElement>(null);
     
      const x = useMotionValue(0);
      const y = useMotionValue(0);
     
      const xSpring = useSpring(x, {
        damping: damping,
        stiffness: swiftness,
      });
     
      const ySpring = useSpring(y, {
        damping: damping,
        stiffness: swiftness,
      });
     
      const transform = useMotionTemplate`rotateX(${xSpring}deg) rotateY(${ySpring}deg)`;
     
      const handleMouseMove = (e: React.MouseEvent) => {
        if (!refParentDiv.current) return;
     
        const rect = refParentDiv.current.getBoundingClientRect();
        const mouseX = (e.clientX - rect.left) * maxRotation;
        const mouseY = (e.clientY - rect.top) * maxRotation;
     
        const rX = (mouseY / rect.height - halfMaxRotation) * -1;
        const rY = mouseX / rect.width - halfMaxRotation;
     
        x.set(rX);
        y.set(rY);
      };
     
      const handleMouseLeave = () => {
        x.set(0);
        y.set(0);
      };
     
      return (
        <div
          ref={refParentDiv}
          className={cn(
            "relative w-full flex justify-center px-6 pt-16 md:pt-20 pb-6 md:pb-20",
            "after:content-[''] after:absolute after:inset-0 after:bg-gradient-to-t dark:after:from-black after:from-white after:to-transparent after:z-30",
            "md:after:content-none md:pointer-events-auto pointer-events-none"
          )}
          onMouseMove={handleMouseMove}
          onMouseLeave={handleMouseLeave}
        >
          <motion.div
            ref={refMotionDiv}
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ duration: 1, ease: "easeOut" }}
            style={{
              transformStyle: "preserve-3d",
              transform,
            }}
            className="shadow-sm dark:shadow-none relative h-[25rem] md:h-[45rem] w-full max-w-[75rem] bg-white dark:bg-black rounded-md border border-zinc-200 dark:border-zinc-800"
          >
            <div
              style={{
                transform: isMedium ? "translateZ(50px)" : "translateZ(0px)",
                transformStyle: "preserve-3d",
              }}
              className="absolute z-20 inset-0 bottom-2 flex rounded-md bg-transparent flex-col"
            >
              {React.Children.map(children, (child) => {
                return React.cloneElement(child as React.ReactElement, { isMedium });
              })}
            </div>
          </motion.div>
        </div>
      );
    }

    Copy the source code

    showcase.tsx

    "use client";
     
    import { useState, useEffect } from "react";
    import { ShowcaseSpotlight } from "@/components/showcase";
    {/*
    import { useTheme } from "next-themes";
    import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
    import { ChartContainer } from "@/components/ui/charts";
    */}
     
    interface User {
      name: string;
      description: string;
      amount: string;
      imageUrl: string;
    }
     
    export function HeaderExample() {
      const menuItems = ["Overview", "Customer", "Products"];
     
      return (
        <div className="flex items-center justify-between w-full font-medium ">
          <div className="flex gap-4 justify-center">
            <div className="border border-zinc-200 dark:border-zinc-800 px-2 py-1.5 flex text-xs rounded-md items-center gap-2 shrink-0 text-black dark:text-white">
              <span className="h-4 w-4 rounded-full bg-gradient-to-br from-black to-white font-medium" />
              John Doe
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="14"
                height="14"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="2"
                strokeLinecap="round"
                strokeLinejoin="round"
                className="lucide lucide-chevron-down md:ml-4"
              >
                <path d="m6 9 6 6 6-6" />
              </svg>
            </div>
            <div className="hidden lg:flex items-center gap-4 text-xs text-neutral-400 font-medium">
              {menuItems.map((item, index) => (
                <span key={index}>{item}</span>
              ))}
            </div>
          </div>
          <div className="border border-zinc-200 dark:border-zinc-800 px-2 py-1.5 flex items-center justify-between text-xs rounded-md gap-2 w-auto sm:w-56 lg:w-64 ml-4 text-neutral-400">
            <span className="flex items-center">
              Search<span className="hidden md:block">...</span>
            </span>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="14"
              height="14"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              strokeWidth="2"
              strokeLinecap="round"
              strokeLinejoin="round"
              className="lucide lucide-search hidden md:block"
            >
              <circle cx="11" cy="11" r="8" />
              <path d="m21 21-4.3-4.3" />
            </svg>
          </div>
        </div>
      );
    }
     
    export function BodyExample() {
      /*
      const { theme } = useTheme();
      const [desktopColor, setDesktopColor] = useState("#f4f4f5");
      const [mobileColor, setMobileColor] = useState("#e4e4e7");
      const [tabletColor, setTabletColor] = useState("#f4f4f5");
     
     
      useEffect(() => {
        if (theme === "dark") {
          setDesktopColor("#18181b");
          setMobileColor("#27272a");
          setTabletColor("#18181b");
        } else {
          setDesktopColor("#f4f4f5");
          setMobileColor("#e4e4e7");
          setTabletColor("#f4f4f5");
        }
      }, [theme]);
     
      const chartData = [
        { month: "January", desktop: 186, mobile: 80, tablet: 50 },
        { month: "February", desktop: 305, mobile: 200, tablet: 130 },
        { month: "March", desktop: 237, mobile: 120, tablet: 148 },
        { month: "April", desktop: 73, mobile: 190, tablet: 78 },
        { month: "May", desktop: 209, mobile: 130, tablet: 27 },
        { month: "June", desktop: 214, mobile: 140, tablet: 156 },
      ];
     
      const chartConfig = {
        desktop: {
          label: "Desktop",
        },
        mobile: {
          label: "Mobile",
        },
        tablet: {
          label: "Tablet",
        },
      };
      */
     
      const users: User[] = [
        {
          name: "John Doe",
          description: "Senior Developer",
          amount: "+$1,999.00",
          imageUrl: "/images/showcase/01.png",
        },
        {
          name: "Jane Smith",
          description: "Project Manager",
          amount: "+$3,200.50",
          imageUrl: "/images/showcase/02.png",
        },
        {
          name: "Emily Johnson",
          description: "UI/UX Designer",
          amount: "+$2,150.75",
          imageUrl: "/images/showcase/03.png",
        },
        {
          name: "Michael Brown",
          description: "Marketing Specialist",
          amount: "+$1,750.00",
          imageUrl: "/images/showcase/04.png",
        },
        {
          name: "Sarah Wilson",
          description: "Content Strategist",
          amount: "+$2,500.00",
          imageUrl: "/images/showcase/05.png",
        },
      ];
     
      return (
        <div className="flex flex-col h-full px-0 md:px-4 py-4 space-y-4 md:mt-auto overflow-hidden w-full text-black dark:text-white">
          <div className="flex justify-between  mt-4">
            <h1 className="text-xl md:text-2xl font-semibold text-left">
              Build your own
            </h1>
            <div className="flex items-center gap-2 font-medium">
              <button className="hidden lg:flex border-zinc-200 dark:border-zinc-800 border px-3 p-2 rounded-md text-xs items-center gap-1 whitespace-nowrap">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="14"
                  height="14"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="lucide lucide-calendar"
                >
                  <path d="M8 2v4" />
                  <path d="M16 2v4" />
                  <rect width="18" height="18" x="3" y="4" rx="2" />
                  <path d="M3 10h18" />
                </svg>
                Jan 01, 2024-Apr 20, 2024
              </button>
              <a
                href="/"
                className="bg-zinc-100 dark:bg-zinc-900 px-2 sm:px-3 text-xs py-1.5 sm:py-2 dark:text-white text-foreground rounded-md font-semibold whitespace-nowrap sm:inline-block hidden"
              >
                <span className="text-black dark:text-white">View in Doc</span>
              </a>
            </div>
          </div>
          <div className="flex w-min items-center p-1 rounded-lg bg-zinc-100 dark:bg-zinc-900 text-sm font-medium">
            <span className="py-1 px-2 bg-white dark:bg-black rounded-lg">
              Overview
            </span>
            <span className="py-1 px-2">Analytics</span>
            <span className="py-1 px-2 sm:inline-block hidden">Reports</span>
            <span className="py-1 px-2 sm:inline-block hidden ">Notifications</span>
          </div>
          <div className="flex items-center gap-4 sm:flex-row flex-col justify-stretch">
            <ShowcaseSpotlight>
              <div className="flex flex-col w-full rounded-xl border border-zinc-200 dark:border-zinc-800 p-3 xl:p-6 h-full shadow-sm dark:shadow-none">
                <div className="flex justify-between pb-2 w-full">
                  <p className="text-xs">Total Revenue</p>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="lucide lucide-dollar-sign text-neutral-400"
                  >
                    <line x1="12" x2="12" y1="2" y2="22" />
                    <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
                  </svg>
                </div>
     
                <div className="flex flex-col items-start w-full">
                  <h3 className="text-xl font-bold">$45,231.89</h3>
                  <p className="text-xs text-zinc-500 dark:text-zinc-400 text-left">
                    +20% from last month
                  </p>
                </div>
              </div>
            </ShowcaseSpotlight>
            <ShowcaseSpotlight>
              <div className="flex flex-col w-full rounded-xl border border-border dark:border-zinc-800 p-3 xl:p-6 h-full shadow-sm dark:shadow-none">
                <div className="flex justify-between pb-2 w-full">
                  <p className="text-xs">Subscriptions</p>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="lucide lucide-users text-neutral-400"
                  >
                    <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
                    <circle cx="9" cy="7" r="4" />
                    <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
                    <path d="M16 3.13a4 4 0 0 1 0 7.75" />
                  </svg>
                </div>
                <div className="flex flex-col items-start w-full">
                  <h3 className="text-xl font-bold">+2350</h3>
                  <p className="text-xs text-zinc-500 dark:text-zinc-400 text-left">
                    +18% from last month
                  </p>
                </div>
              </div>
            </ShowcaseSpotlight>
            <ShowcaseSpotlight className="hidden lg:flex">
              <div className="hidden lg:flex flex-col w-full rounded-xl border border-border dark:border-zinc-800 p-3 xl:p-6 h-full shadow-sm dark:shadow-none">
                <div className="flex justify-between pb-2 w-full">
                  <p className="text-xs">Sales</p>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="lucide lucide-credit-card text-neutral-400"
                  >
                    <rect width="20" height="14" x="2" y="5" rx="2" />
                    <line x1="2" x2="22" y1="10" y2="10" />
                  </svg>
                </div>
                <div className="flex flex-col items-start w-full">
                  <h3 className="text-xl font-bold">+12,234</h3>
                  <p className="text-xs text-zinc-500 dark:text-zinc-400 text-left">
                    +19% from last month
                  </p>
                </div>
              </div>
            </ShowcaseSpotlight>
          </div>
          <div className="gap-4 w-full flex items-stretch h-full">
            <div className="w-1/2 hidden lg:flex border-zinc-200 dark:border-zinc-800 border rounded-xl relative shadow-sm dark:shadow-none">
            {/* 
              <ChartContainer
                config={chartConfig}
                className="absolute inset-0 mt-auto px-6 pt-6"
              >
                <BarChart accessibilityLayer data={chartData}>
                  <CartesianGrid vertical={false} />
                  <XAxis
                    dataKey="month"
                    tickLine={false}
                    tickMargin={10}
                    axisLine={false}
                    tickFormatter={(value) => value.slice(0, 3)}
                  />
                  <Bar dataKey="desktop" fill={desktopColor} radius={4} />
                  <Bar dataKey="mobile" fill={mobileColor} radius={4} />
                  <Bar dataKey="tablet" fill={tabletColor} radius={4} />
                </BarChart>
              </ChartContainer>
            */}
            </div>
     
            <div className="hidden w-full lg:w-1/2 md:flex rounded-xl flex-col px-6 py-4 border-zinc-200 dark:border-zinc-800 border items-start shadow-sm dark:shadow-none">
              <h3 className="font-semibold text-base">Real Component</h3>
              <p className="text-xs text-zinc-500 dark:text-zinc-400 mb-2">
                Discover Showcase.
              </p>
              <div className="w-full h-full flex flex-col justify-around">
                {users.map((user, index) => (
                  <div key={index} className="flex gap-2 py-3 w-full items-center">
                    <div className="h-8 w-8 flex items-center justify-center rounded-full">
                      <img
                        src={user.imageUrl}
                        alt={user.name}
                        className="h-full w-full rounded-full object-cover"
                      />
                    </div>
                    <div className="flex-col flex items-start">
                      <span className="text-xs font-semibold">{user.name}</span>
                      <span className="text-xs text-zinc-400">
                        {user.description}
                      </span>
                    </div>
                    <span className="font-medium ml-auto text-sm">
                      {user.amount}
                    </span>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      );
    }

    Props

    Showcase props

    PropTypeDescriptionDefault
    childrenReact.ReactNodeElements displayed inside the showcase.-
    dampingnumberControls the spring animation damping of the rotation effect.20
    swiftnessnumberControls the spring animation stiffness of the rotation effect.80
    maxRotationnumberMaximum rotation angle applied to the showcase on mouse movement.25

    ShowcaseSpotlight props

    PropTypeDescriptionDefault
    childrenReact.ReactNodeElements to be highlighted with the spotlight effect.-
    classNamestringAdditional classes for custom styling of the spotlight.-

    ShowcaseHeader props

    PropTypeDescriptionDefault
    childrenReact.ReactNodeContent to be displayed in the header section of the showcase.-

    ShowcaseBody props

    PropTypeDescriptionDefault
    childrenReact.ReactNodeMain content to be displayed in the body of the showcase.-
    isMediumbooleanDetermines if the screen size is medium or larger, adjusting layout accordingly.-

    Credits

  1. This component is inspired by Webflow
  2. The design of this demo component is inspired by the homepage of ShadcnUI