Shared Layout Background

December 2025

Smooth background component animated using motions's shared layout on hover. Enter and exit animations applied only when mouse enters of leaves the container, otherwise the background animates between the items with shared layout.

Demo

Code

"use client";

import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { Children, cloneElement, isValidElement, useId, useState } from "react";

export function SharedLayoutBackground({
  children,
}: {
  children: React.ReactNode;
}) {
  const [activeId, setActiveId] = useState<number | string | null>(null);
  const uniqueId = useId();

  return (
    <div
      onMouseLeave={() => {
        setActiveId(null);
      }}
      className="flex w-full flex-col"
    >
      {Children.toArray(children)
        .filter(isValidElement)
        .map((child: React.ReactElement, index) =>
          cloneElement(
            child,
            {
              key: index,
              className: cn("relative", child.props.className),
              onMouseEnter: () => setActiveId(index),
            },
            <>
              <AnimatePresence custom={activeId !== null}>
                {activeId !== null && (
                  <motion.div
                    variants={variants}
                    initial="initial"
                    animate="animate"
                    exit="exit"
                    className="pointer-events-none absolute -inset-x-5 inset-y-0"
                  >
                    {activeId === index && (
                      <motion.div
                        layoutId={`background-${uniqueId}`}
                        transition={{
                          type: "spring",
                          stiffness: 205,
                          damping: 22,
                        }}
                        className="pointer-events-none size-full rounded-2xl bg-muted"
                      ></motion.div>
                    )}
                  </motion.div>
                )}
              </AnimatePresence>

              <div className="relative z-10 col-span-full grid grid-cols-subgrid">
                {child.props.children}
              </div>
            </>
          )
        )}
    </div>
  );
}

const variants = {
  initial: {
    opacity: 0,
    filter: "blur(6px)",
  },
  animate: {
    opacity: 1,
    filter: "blur(0px)",
  },
  exit: (isActive: boolean) =>
    !isActive
      ? {
          opacity: 0,
          filter: "blur(6px)",
        }
      : {},
};