{"slug":"shared-layout-bg","name":"Shared Layout Background","description":"A pill that glides between hovered items via motion's shared layout, with blur enter/exit.","category":"motion","source_url":"https://beui.dev/r/shared-layout-bg/raw","detail_url":"https://beui.dev/r/shared-layout-bg","raw_url":"https://beui.dev/r/shared-layout-bg/raw","page_url":"https://beui.dev/components/motion/shared-layout-bg","markdown_url":"https://beui.dev/components/motion/shared-layout-bg.md","published_at":"2026-05-17","updated_at":"2026-09-22","dependencies":["clsx","lucide-react","motion","react","tailwind-merge"],"internal":["@/components/motion/shared-layout-bg","@/lib/ease","@/lib/utils"],"files":[{"path":"components/motion/shared-layout-bg.tsx","type":"component","content":"\"use client\";\n// beui.dev/components/motion/shared-layout-bg\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  forwardRef,\n  type HTMLAttributes,\n  isValidElement,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useId,\n  useState,\n} from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SharedLayoutBgProps\n  extends Omit<HTMLAttributes<HTMLElement>, \"children\"> {\n  children: ReactNode;\n  /** Semantic container used for the children. */\n  as?: \"div\" | \"ul\";\n  /** Tailwind class applied to the moving pill. Defaults to a subtle foreground tint. */\n  pillClassName?: string;\n  /** Horizontal inset of the pill relative to each row (px). Default 20. */\n  inset?: number;\n  /** Optional positioning override for the pill wrapper inside each item. */\n  pillContainerClassName?: string;\n}\n\nconst variants: Variants = {\n  initial: { opacity: 0, filter: \"blur(6px)\" },\n  animate: { opacity: 1, filter: \"blur(0px)\" },\n  exit: (isActive: boolean) =>\n    !isActive ? { opacity: 0, filter: \"blur(6px)\" } : {},\n};\n\nconst reducedVariants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: (isActive: boolean) => (!isActive ? { opacity: 0 } : {}),\n};\n\nexport const SharedLayoutBg = forwardRef<HTMLElement, SharedLayoutBgProps>(\n  function SharedLayoutBg(\n    {\n      children,\n      as = \"div\",\n      className,\n      onMouseLeave,\n      pillClassName,\n      pillContainerClassName,\n      inset = 20,\n      ...props\n    },\n    forwardedRef,\n  ) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const uid = useId();\n  const reduce = useReducedMotion();\n\n    const renderedChildren = Children.toArray(children)\n      .filter(isValidElement)\n      .map((child, index) => {\n        const el = child as ReactElement<{\n          className?: string;\n          onMouseEnter?: () => void;\n          children?: ReactNode;\n        }>;\n        const childKey = el.key ? String(el.key) : `item-${index}`;\n        return cloneElement(\n          el,\n          {\n            key: childKey,\n            className: cn(\"relative\", el.props.className),\n            onMouseEnter: () => {\n              el.props.onMouseEnter?.();\n              setActiveId(childKey);\n            },\n          },\n          <>\n            <AnimatePresence custom={activeId !== null}>\n              {activeId !== null ? (\n                <motion.div\n                  variants={reduce ? reducedVariants : variants}\n                  initial=\"initial\"\n                  animate=\"animate\"\n                  exit=\"exit\"\n                  custom={activeId !== null}\n                  className={cn(\n                    \"pointer-events-none absolute inset-y-0\",\n                    pillContainerClassName,\n                  )}\n                  style={{ left: -inset, right: -inset }}\n                >\n                  {activeId === childKey ? (\n                    <motion.div\n                      layoutId={`shared-bg-${uid}`}\n                      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                      className={cn(\n                        \"pointer-events-none h-full w-full rounded-2xl bg-muted/80\",\n                        pillClassName,\n                      )}\n                    />\n                  ) : null}\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n            <div className=\"relative z-10\">{el.props.children}</div>\n          </>,\n        );\n      });\n\n    const handleMouseLeave = (event: MouseEvent<HTMLElement>) => {\n      setActiveId(null);\n      onMouseLeave?.(event);\n    };\n\n    // layoutRoot scopes the pill's layout projection to this list, so fixed or\n    // scrolled ancestors can't smear scroll offsets into its movement.\n    return as === \"ul\" ? (\n      <motion.ul\n        {...(props as HTMLMotionProps<\"ul\">)}\n        ref={forwardedRef as Ref<HTMLUListElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.ul>\n    ) : (\n      <motion.div\n        {...(props as HTMLMotionProps<\"div\">)}\n        ref={forwardedRef as Ref<HTMLDivElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.div>\n    );\n  },\n);\n"},{"path":"lib/ease.ts","type":"util","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"util","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/previews/motion/shared-layout-bg.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { ArrowUpRight } from \"lucide-react\";\nimport { SharedLayoutBg } from \"@/components/motion/shared-layout-bg\";\n\nconst items = [\n  { title: \"Inbox\", body: \"12 unread threads, 3 mentions today.\" },\n  { title: \"Drafts\", body: \"4 posts waiting for a final pass.\" },\n  { title: \"Releases\", body: \"Last shipped 2 days ago, v0.4.1.\" },\n  { title: \"Billing\", body: \"Plan renews on the 1st of next month.\" },\n];\n\nexport function SharedLayoutBgPreview() {\n  return (\n    <div className=\"w-full max-w-lg px-2\">\n      <SharedLayoutBg>\n        {items.map((it) => (\n          <button\n            type=\"button\"\n            key={it.title}\n            className=\"group flex flex-col gap-1 px-2 py-3 text-left\"\n          >\n            <div className=\"flex items-center justify-between gap-3\">\n              <span className=\"text-sm font-medium text-foreground\">{it.title}</span>\n              <ArrowUpRight className=\"h-3.5 w-3.5 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5\" />\n            </div>\n            <p className=\"text-sm text-muted-foreground\">{it.body}</p>\n          </button>\n        ))}\n      </SharedLayoutBg>\n    </div>\n  );\n}\n"}]}