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)",
}
: {},
};