ComponentsChangelogPro

    Getting Started

    Animated Cards

    Components

    Backgrounds

    3d & Shaders

    Text Animations

    Buttons

    Docs
    Expandable Card

    Expandable Card

    A versatile and engaging UI component that allows users to explore content in a more immersive way.

    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 source code

    expandable-card.tsx

    "use client";
     
    import * as React from "react";
    import { AnimatePresence, motion } from "framer-motion";
    import { cn } from "@/lib/utils";
     
    interface ExpandableCardProps {
      title: string;
      src: string;
      description: string;
      children?: React.ReactNode;
      className?: string;
      [key: string]: any;
    }
     
    export default function ExpandableCard({
      title,
      src,
      description,
      children,
      className,
      ...props
    }: ExpandableCardProps) {
      const [active, setActive] = React.useState(false);
      const localRef = React.useRef<HTMLDivElement>(null);
      const id = React.useId();
     
      React.useEffect(() => {
        const onKeyDown = (event: KeyboardEvent) => {
          if (event.key === "Escape") {
            setActive(false);
          }
        };
     
        const handleClickOutside = (event: MouseEvent | TouchEvent) => {
          if (localRef.current && !localRef.current.contains(event.target as Node)) {
            setActive(false);
          }
        };
     
        window.addEventListener("keydown", onKeyDown);
        document.addEventListener("mousedown", handleClickOutside);
        document.addEventListener("touchstart", handleClickOutside);
     
        return () => {
          window.removeEventListener("keydown", onKeyDown);
          document.removeEventListener("mousedown", handleClickOutside);
          document.removeEventListener("touchstart", handleClickOutside);
        };
      }, []);
     
      return (
        <>
          <AnimatePresence>
            {active && (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="fixed inset-0 bg-white/50 dark:bg-black/50 backdrop-blur-md h-full w-full z-10"
              />
            )}
          </AnimatePresence>
          <AnimatePresence>
            {active && (
              <div className="fixed inset-0 grid place-items-center z-[100] mt-16">
                <motion.div
                  layoutId={`card-${title}-${id}`}
                  ref={localRef}
                  className={cn(
                    "w-full max-w-[850px] h-full flex flex-col  overflow-auto [scrollbar-width:none] [-ms-overflow-style:none] [-webkit-overflow-scrolling:touch] before:content-[''] before:pointer-events-none before:absolute before:inset-0 before:bg-gradient-to-t from-white dark:from-black to-20% before:z-10 shadow-sm dark:shadow-none sm:rounded-t-md  bg-white dark:bg-black",
                    className
                  )}
                  {...props}
                >
                  <motion.div layoutId={`image-${title}-${id}`}>
                    <img
                      src={src}
                      alt={title}
                      className="w-full h-80 lg:h-80 object-cover object-center"
                    />
                  </motion.div>
     
                  <div className="relative h-full">
                    <div className="flex justify-between items-center p-8 h-auto">
                      <div>
                        <motion.p
                          layoutId={`description-${description}-${id}`}
                          className="text-zinc-500 dark:text-zinc-400 text-lg mb-0.5 font-medium"
                        >
                          {description}
                        </motion.p>
                        <motion.h3
                          layoutId={`title-${title}-${id}`}
                          className="font-bold text-neutral-700 dark:text-neutral-200 text-4xl"
                        >
                          {title}
                        </motion.h3>
                      </div>
     
                      <motion.button
                        layoutId={`button-${title}-${id}`}
                        className="h-9 w-9 shrink-0 flex items-center justify-center rounded-full bg-white dark:bg-black text-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-950 dark:text-white border border-neutral-200 dark:border-neutral-700 transition-colors"
                        onClick={() => setActive(false)}
                      >
                        <motion.div
                          animate={{ rotate: active ? 45 : 0 }}
                          transition={{ duration: 0.4 }}
                        >
                          <svg
                            xmlns="http://www.w3.org/2000/svg"
                            width="16"
                            height="16"
                            viewBox="0 0 24 24"
                            fill="none"
                            stroke="currentColor"
                            strokeWidth="1.5"
                            strokeLinecap="round"
                            strokeLinejoin="round"
                            className="lucide lucide-plus"
                          >
                            <path d="M5 12h14" />
                            <path d="M12 5v14" />
                          </svg>
                        </motion.div>
                      </motion.button>
                    </div>
                    <div className="relative px-8">
                      <motion.div
                        layout
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        exit={{ opacity: 0 }}
                        className="text-zinc-500 dark:text-zinc-400 text-base pb-10 flex flex-col items-start gap-4 overflow-auto "
                      >
                        {children}
                      </motion.div>
                    </div>
                  </div>
                </motion.div>
              </div>
            )}
          </AnimatePresence>
     
          <motion.div
            layoutId={`card-${title}-${id}`}
            onClick={() => setActive(true)}
            className={cn(
              "p-3 flex flex-col justify-between items-center bg-white shadow-sm dark:shadow-none dark:bg-black rounded-md cursor-pointer border border-neutral-100 dark:border-neutral-800",
              className
            )}
          >
            <div className="flex gap-4 flex-col">
              <motion.div layoutId={`image-${title}-${id}`}>
                <img
                  src={src}
                  alt={title}
                  className="w-64 h-56 rounded object-cover object-center"
                />
              </motion.div>
              <div className="flex justify-between items-center">
                <div className="flex flex-col">
                  <motion.p
                    layoutId={`description-${description}-${id}`}
                    className="text-zinc-500 dark:text-zinc-400 md:text-left text-sm font-medium"
                  >
                    {description}
                  </motion.p>
                  <motion.h3
                    layoutId={`title-${title}-${id}`}
                    className="text-neutral-800 dark:text-neutral-200 md:text-left font-semibold"
                  >
                    {title}
                  </motion.h3>
                </div>
                <motion.button
                  layoutId={`button-${title}-${id}`}
                  className="h-8 w-8 shrink-0 flex items-center justify-center rounded-full bg-white dark:bg-black text-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-950 dark:text-white border border-neutral-200 dark:border-neutral-700 transition-colors"
                >
                  <motion.div
                    animate={{ rotate: active ? 45 : 0 }}
                    transition={{ duration: 0.4 }}
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="16"
                      height="16"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      className="lucide lucide-plus"
                    >
                      <path d="M5 12h14" />
                      <path d="M12 5v14" />
                    </svg>
                  </motion.div>
                </motion.button>
              </div>
            </div>
          </motion.div>
        </>
      );
    }

    Props

    PropTypeDescriptionDefault
    titlestringThe title of the card, displayed prominently at the top of the card.-
    srcstringThe source URL for the image displayed in the card.-
    descriptionstringA brief description displayed below the title in the card.-
    childrenReact.ReactNodeThe content inside the expandable section of the card, shown when expanded.-
    classNamestringOptional custom class names for styling the card.-

    Credits

  1. This component is inspired by Linear
  2. The images are from Pexels