shoppablecn

Hotspot Image

Interactive scene images with percentage-based hotspot pins that open tooltips, links, or embedded product cards.

HotspotImage turns editorial scenes and product photography into interactive entry points. It keeps the source image at its natural dimensions, places pins using percentage coordinates, and lets each pin open any content you need.

Installation

npx shadcn@latest add @shoppablecn/hotspot-image

Examples

Basic scene

Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art
import { HotspotImage } from "@/components/ui/hotspot-image"
import { HotspotPin } from "@/components/ui/hotspot-pin"
import { HotspotTooltip } from "@/components/ui/hotspot-tooltip"

export function Example() {
  const scene = {
    src: "/scenes/living-room.jpg",
    alt: "Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art",
    width: 964,
    height: 1277,
  }

  return (
    <HotspotImage
      alt={scene.alt}
      height={scene.height}
      src={scene.src}
      width={scene.width}
    >
      <HotspotPin x={16} y={11}>
        <HotspotTooltip
          description="A matte black floor lamp that pools warm ambient light over the chair and desk side of the room."
          title="Arc floor lamp"
        />
      </HotspotPin>
    </HotspotImage>
  )
}

Pin variants

Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art
import { HotspotImage } from "@/components/ui/hotspot-image"
import { HotspotPin } from "@/components/ui/hotspot-pin"
import { HotspotTooltip } from "@/components/ui/hotspot-tooltip"

export function Example() {
  const scene = {
    src: "/scenes/living-room.jpg",
    alt: "Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art",
    width: 964,
    height: 1277,
  }

  return (
    <HotspotImage
      alt={scene.alt}
      height={scene.height}
      src={scene.src}
      width={scene.width}
    >
      <HotspotPin variant="plus" x={40} y={58}>
        <HotspotTooltip description="Primary product pin on the lounge chair." title="Harbor lounge chair" />
      </HotspotPin>
      <HotspotPin variant="dot" x={33} y={88}>
        <HotspotTooltip description="Secondary detail pin on the retro controller." title="Retro Wireless Controller" />
      </HotspotPin>
    </HotspotImage>
  )
}

Content types in pins

Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art
import { HotspotImage } from "@/components/ui/hotspot-image"
import { HotspotLink } from "@/components/ui/hotspot-link"
import { HotspotPin } from "@/components/ui/hotspot-pin"
import { HotspotTooltip } from "@/components/ui/hotspot-tooltip"
import { ProductCard } from "@/components/ui/product-card"
import type { Product } from "@/components/ui/types"

export function Example() {
  const scene = {
    src: "/scenes/living-room.jpg",
    alt: "Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art",
    width: 964,
    height: 1277,
  }

  const product = {
    id: "controller-001",
    title: "Retro Wireless Controller",
    images: [
      {
        src: "/products/retro-wireless-controller.jpg",
        alt: "Retro Wireless Controller with colorful face buttons",
      },
    ],
    price: { amount: 5900, currency: "USD" },
  } satisfies Product

  return (
    <HotspotImage
      alt={scene.alt}
      height={scene.height}
      src={scene.src}
      width={scene.width}
    >
      <HotspotPin x={16} y={11}>
        <HotspotTooltip
          description="The oversized lamp softens the gaming setup with warm light and gives the corner a more editorial feel."
          title="Arc floor lamp"
        />
      </HotspotPin>
      <HotspotPin variant="dot" x={71} y={18}>
        <HotspotLink href="/docs/components/product-card" label="See the full room edit" />
      </HotspotPin>
      <HotspotPin x={33} y={88}>
        <div className="w-[320px]">
          <ProductCard product={product} variants="none" />
        </div>
      </HotspotPin>
    </HotspotImage>
  )
}

ProductCard overlay inside a pin

Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art
import { HotspotImage } from "@/components/ui/hotspot-image"
import { HotspotPin } from "@/components/ui/hotspot-pin"
import { ProductCard } from "@/components/ui/product-card"
import type { Product, ProductVariant } from "@/components/ui/types"

export function Example() {
  const scene = {
    src: "/scenes/living-room.jpg",
    alt: "Gaming lounge scene with an ivory chair, pixel heart pillow, floor lamp, retro controllers, and rocket wall art",
    width: 964,
    height: 1277,
  }

  const variants = [
    {
      id: "upholstery",
      name: "Upholstery",
      type: "swatch",
      required: true,
      options: [
        { label: "Oat", value: "oat", swatch: "#e6ddd1" },
        { label: "Pebble", value: "pebble", swatch: "#b8b0a4" },
      ],
    },
    {
      id: "delivery",
      name: "Delivery",
      type: "radio",
      required: true,
      options: [
        { label: "Standard", value: "standard" },
        { label: "White glove", value: "white-glove" },
      ],
    },
  ] satisfies ProductVariant[]

  const product = {
    id: "chair-001",
    title: "Harbor Lounge Chair",
    images: [
      {
        src: "/products/harbor-lounge-chair.jpg",
        alt: "Harbor Lounge Chair in oat boucle",
      },
    ],
    price: { amount: 64900, currency: "USD" },
    variants,
  } satisfies Product

  return (
    <HotspotImage
      alt={scene.alt}
      height={scene.height}
      src={scene.src}
      width={scene.width}
    >
      <HotspotPin x={40} y={58}>
        <div className="w-[320px]">
          <ProductCard
            onAddToCart={() => {}}
            product={product}
            variants="overlay"
          />
        </div>
      </HotspotPin>
    </HotspotImage>
  )
}

Props

HotspotImage

Prop

Type

HotspotPin

Prop

Type

HotspotTooltip

Prop

Type

Prop

Type

Accessibility

  • Pins are real interactive buttons with labels.
  • Popovers close on Escape and return focus to the trigger.
  • Scene regions remain horizontally scrollable instead of collapsing the image.
  • Embedded ProductCard content keeps its own keyboard behavior inside the popover.
  • ProductCard can be rendered directly inside hotspot pins.

On this page