shadimage
Spectrum UI
componentbento-gridsource: ok · refreshed

Bento Grid

A premium interactive Bento Grid component

npx shadcn add "https://ui.spectrumhq.in/r/bento-grid.json"
free

Where it goes

bento-grid

Visual style

bento

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion
iconslucide-react

Files4 · source cached 10/8/2026, 4:09:37 AM

app/registry/bento-grid/bento-grid.tsx
import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
import React, { ReactNode } from "react"

export interface BentoGridProps {
  className?: string
  children?: ReactNode
}

const containerVariants = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.1,
    },
  },
}

export function BentoGrid({ className, children }: BentoGridProps) {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, margin: "-50px" }}
      className={cn(
        "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 w-full max-w-7xl mx-auto",
        className
      )}
    >
      {children}
    </motion.div>
  )
}
app/registry/bento-grid/bento-card.tsx
"use client"

import { cn } from "@/lib/utils"
import { motion, useMotionValue, useSpring, useTransform, type Transition } from "framer-motion"
import React, { ReactNode, useRef, useState } from "react"
import { Spotlight } from "./spotlight"
import { BorderBeam } from "./border-beam"

const SPRING_TACTILE: Transition = {
  type: "spring",
  stiffness: 400,
  damping: 25,
  mass: 1,
}

const SPRING_ENTRANCE: Transition = {
  type: "spring",
  stiffness: 260,
  damping: 20,
}

export interface BentoCardProps {
  className?: string
  children?: ReactNode
  title?: ReactNode
  description?: ReactNode
  icon?: ReactNode
  background?: ReactNode
  colSpan?: number
  rowSpan?: number
  tilt?: boolean
  spotlight?: boolean
  borderAnim?: boolean
}

const itemVariants = {
  hidden: { opacity: 0, y: 12, scale: 0.98 },
  visible: { 
    opacity: 1, 
    y: 0, 
    scale: 1, 
    transition: SPRING_ENTRANCE 
  },
  hover: {
    scale: 1.01,
    y: -4,
    transition: SPRING_TACTILE
  }
}

export function BentoCard({
  className,
  children,
  title,
  description,
  icon,
  background,
  colSpan = 1,
  rowSpan = 1,
  tilt = false,
  spotlight = true,
  borderAnim = true,
}: BentoCardProps) {
  const ref = useRef<HTMLDivElement>(null)
  const [isHovered, setIsHovered] = useState(false)

  const spotlightX = useMotionValue(0)
  const spotlightY = useMotionValue(0)

  const tiltX = useMotionValue(0)
  const tiltY = useMotionValue(0)

  // Smooth springs for tilt
  const smoothTiltX = useSpring(tiltX, { stiffness: 300, damping: 30 })
  const smoothTiltY = useSpring(tiltY, { stiffness: 300, damping: 30 })

  const rotateX = useTransform(smoothTiltY, [-150, 150], [5, -5])
  const rotateY = useTransform(smoothTiltX, [-150, 150], [-5, 5])

  function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
    if (!ref.current) return
    const rect = ref.current.getBoundingClientRect()
    
    // For Spotlight (relative to top left)
    spotlightX.set(e.clientX - rect.left)
    spotlightY.set(e.clientY - rect.top)

    // For Tilt (relative to center)
    if (tilt) {
      const centerX = rect.width / 2
      const centerY = rect.height / 2
      tiltX.set(e.clientX - rect.left - centerX)
      tiltY.set(e.clientY - rect.top - centerY)
    }
  }

  function handleMouseEnter() {
    setIsHovered(true)
  }

  function handleMouseLeave() {
    setIsHovered(false)
    if (tilt) {
      tiltX.set(0)
      tiltY.set(0)
    }
  }

  // Calculate CSS grid spans
  const colSpanClass = {
    1: "md:col-span-1",
    2: "md:col-span-2",
    3: "md:col-span-3",
    4: "md:col-span-4",
  }[colSpan] || "md:col-span-1"

  const rowSpanClass = {
    1: "row-span-1",
    2: "row-span-2",
    3: "row-span-3",
  }[rowSpan] || "row-span-1"

  return (
    <motion.div
      ref={ref}
      variants={itemVariants}
      onMouseMove={handleMouseMove}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      whileHover="hover"
      whileTap={{ scale: 0.98 }}
      transition={SPRING_TACTILE}
      style={{
        ...(tilt && { rotateX, rotateY }),
        transformStyle: "preserve-3d",
      }}
      className={cn(
        "group relative flex flex-col justify-between overflow-hidden rounded-2xl",
        "bg-neutral-50 dark:bg-[#0C0C0C]",
        "border border-neutral-200 dark:border-neutral-800",
        "shadow-xs transition-shadow duration-300 hover:shadow-lg hover:shadow-neutral-200/50 dark:hover:shadow-black/50",
        "col-span-1",
        colSpanClass,
        rowSpanClass,
        className
      )}
    >
      {/* Spotlight Effect */}
      {spotlight && (
        <Spotlight 
          mouseX={spotlightX} 
          mouseY={spotlightY} 
          isHovered={isHovered} 
        />
      )}

      {/* Border Beam */}
      {borderAnim && <BorderBeam isHovered={isHovered} />}

      {/* Background Effect */}
      {background && (
        <div className="absolute inset-0 z-0 opacity-50 transition-opacity duration-300 group-hover:opacity-100">
          {background}
        </div>
      )}

      {/* Content Container */}
      <div className="relative z-10 flex h-full flex-col justify-between p-4 sm:p-5 xl:p-6">
        {/* Render children first if it's the main visual */}
        <div className="flex-1">{children}</div>

        {/* Header / Footer text area */}
        {(title || description || icon) && (
          <div className={cn("flex items-start space-x-4", children ? "mt-4 sm:mt-6" : "")}>
            {icon && (
              <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-neutral-200/50 dark:bg-neutral-800/50 text-neutral-900 dark:text-neutral-100">
                {icon}
              </div>
            )}
            <div>
              {title && (
                <h3 className="text-lg font-medium tracking-tight text-neutral-900 dark:text-neutral-100">
                  {title}
                </h3>
              )}
              {description && (
                <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
                  {description}
                </p>
              )}
            </div>
          </div>
        )}
      </div>
    </motion.div>
  )
}
app/registry/bento-grid/spotlight.tsx
"use client"

import { cn } from "@/lib/utils"
import { motion, MotionValue, useMotionTemplate } from "framer-motion"
import React from "react"

export interface SpotlightProps {
  className?: string
  mouseX: MotionValue<number>
  mouseY: MotionValue<number>
  isHovered: boolean
  color?: string
}

export function Spotlight({
  className,
  mouseX,
  mouseY,
  isHovered,
  color = "rgba(255, 255, 255, 0.06)",
}: SpotlightProps) {
  const background = useMotionTemplate`
    radial-gradient(
      350px circle at ${mouseX}px ${mouseY}px,
      ${color},
      transparent 80%
    )
  `

  return (
    <motion.div
      className={cn(
        "pointer-events-none absolute -inset-px z-0 transition-opacity duration-300",
        isHovered ? "opacity-100" : "opacity-0",
        className
      )}
      style={{ background }}
    />
  )
}
app/registry/bento-grid/border-beam.tsx
"use client"

import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
import React from "react"

export interface BorderBeamProps {
  className?: string
  duration?: number
  borderWidth?: number
  colorFrom?: string
  colorTo?: string
  isHovered?: boolean
}

export function BorderBeam({
  className,
  duration = 8,
  borderWidth = 1.5,
  colorFrom = "rgba(163, 163, 163, 0)",
  colorTo = "rgba(163, 163, 163, 0.4)",
  isHovered = false,
}: BorderBeamProps) {
  return (
    <div
      className={cn(
        "pointer-events-none absolute inset-0 z-0 rounded-[inherit] border border-transparent",
        "[mask-clip:padding-box,border-box]! mask-intersect! [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
        className
      )}
      style={{
        borderWidth: borderWidth,
      }}
    >
      <motion.div
        className="absolute left-1/2 top-1/2 aspect-square w-[250%] -translate-x-1/2 -translate-y-1/2"
        style={{
          background: `conic-gradient(from 0deg, ${colorFrom} 0%, ${colorTo} 50%, ${colorFrom} 100%)`,
          opacity: isHovered ? 1 : 0.3,
          transition: "opacity 0.3s ease",
        }}
        animate={{ rotate: 360 }}
        transition={{
          repeat: Infinity,
          ease: "linear",
          duration: duration,
        }}
      />
    </div>
  )
}

More from Spectrum UI

Browse all components in this library →