---
title: Image Hotspots
description: An image with markers that name what it shows, each a button that hands its target to the page, and on the phone a strip of the targets as links.
visibility: guest
draft: false
---

# Image Hotspots

`<x-wirekit::image-hotspots>` lays markers over an image: a shop shows the products in a room, a guide names the parts of a machine. Each marker is a button named after its target. On hover and focus its name shows beside it, and a press sends `wirekit:image-hotspots-select`, so the page decides what opens, a quick view, a drawer or another page. On the phone a strip under the image lists the targets as links.

## Basic Usage

Give the image, its size in pixels and the markers, each with its position in percent of the image and the name of its target:

:::preview{title="A Room With Three Products"}
<x-wirekit::image-hotspots
    src="/placeholder/1600x1000?bg=e2e8f0&fg=334155&label=Living+room&gradient=none"
    alt="A living room with a sofa, a floor lamp and a rug"
    width="1600"
    height="1000"
    loading="eager"
    :markers="[
        ['x' => 24, 'y' => 58, 'label' => 'Sofa Bergen', 'target' => 'sofa-bergen', 'href' => '#sofa-bergen'],
        ['x' => 71, 'y' => 34, 'label' => 'Floor lamp Arc', 'target' => 'lamp-arc', 'href' => '#lamp-arc'],
        ['x' => 48, 'y' => 86, 'label' => 'Rug Dune', 'target' => 'rug-dune', 'href' => '#rug-dune'],
    ]"
/>
:::

The markers come in the order Tab reaches them, so put them in the order a reader should meet them. A marker right of the middle shows its name on its left, the others on their right. A name longer than the room between its marker and the edge of the image wraps onto a second line, so it never runs past the image, not even on a phone.

## Markers

Each marker is an array:

| Key | Type | Required | Description |
| --- | --- | --- | --- |
| `x`, `y` | `int\|float` | yes | Where the center of the marker stands, in percent of the image's width and height, from its top left corner |
| `label` | `string` | yes | The name of the target: the marker's accessible name, the text beside it and the text of its card |
| `target` | `string\|int` | no | What the page needs to open the target, an id or a key. It comes back in the event |
| `href` | `string` | no | The link of the target's card in the strip. Without it the card is text |
| `image` | `string` | no | A thumbnail on the target's card |
| `phoneX`, `phoneY` | `int\|float` | no | The position on the phone's file. They fall back to `x` and `y` |

A marker without a `label` would be a button nobody can identify, and one without a position has no place, so both are reported and left out. A position outside 0 to 100 is reported and held at the edge.

## A File for the Phone

A phone often shows another crop of the same scene. `phone-src` gives it, with its own size, and every marker takes its position on it from `phoneX` and `phoneY`. The browser switches the file below `48rem`, and the markers move at the same width:

```blade
<x-wirekit::image-hotspots
    src="/img/living-room.jpg"
    alt="A living room with a sofa, a floor lamp and a rug"
    width="1600"
    height="1000"
    phone-src="/img/living-room-phone.jpg"
    phone-width="900"
    phone-height="1200"
    :markers="[
        ['x' => 24, 'y' => 58, 'phoneX' => 30, 'phoneY' => 70, 'label' => 'Sofa Bergen', 'target' => 'sofa-bergen'],
        ['x' => 71, 'y' => 34, 'phoneX' => 64, 'phoneY' => 22, 'label' => 'Floor lamp Arc', 'target' => 'lamp-arc'],
    ]"
/>
```

The image takes no ratio of its own: its box is the image, so a percentage of the box is a percentage of the image, on either file.

## The Strip on the Phone

Below `48rem` a strip under the image lists the targets, each a link where it has an `href`, with its thumbnail where it has an `image`. A press on a marker marks its card and scrolls the strip until the card is in view. The strip is always in the markup, also where the stylesheet hides it, so screen readers and search engines read the targets as links. `:strip="false"` leaves it out:

:::preview{title="Markers Without a Strip"}
<x-wirekit::image-hotspots
    src="/placeholder/1200x800?bg=fde68a&fg=78350f&label=Workshop&gradient=none"
    alt="A workbench with a drill, a vice and a lamp"
    width="1200"
    height="800"
    :strip="false"
    :markers="[
        ['x' => 30, 'y' => 45, 'label' => 'Cordless drill'],
        ['x' => 66, 'y' => 62, 'label' => 'Bench vice'],
    ]"
/>
:::

## Opening the Target

A press sends `wirekit:image-hotspots-select` from the marker, and it bubbles. Its `detail` carries `index`, `target` and `label`. In a Livewire component the page opens what it needs:

```blade
<x-wirekit::image-hotspots
    src="/img/living-room.jpg"
    alt="A living room with a sofa, a floor lamp and a rug"
    width="1600"
    height="1000"
    :markers="$hotspots"
    x-on:wirekit:image-hotspots-select="$wire.openQuickView($event.detail.target)"
/>
```

```php
public function openQuickView(string $product): void
{
    $this->quickView = Product::where('slug', $product)->firstOrFail();
}
```

The marker takes the focus when it is pressed, also in Safari, which does not focus a button it clicks. A quick view in a [modal](/components/modal) or a [drawer](/components/drawer) hands the focus back to it when it closes.

## In a Carousel

The component holds no state outside itself, so it works as a slide of a [carousel](/components/carousel), one image of a campaign after another.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `string` | — | The image's address. Without one nothing renders, and it is reported |
| `alt` | `string` | `''` | The image's text alternative |
| `srcset` | `string\|null` | `null` | Responsive sources of the image |
| `sizes` | `string\|null` | `null` | The width the image takes in the layout, for `srcset` |
| `width` | `int\|null` | `null` | The image's width in pixels, which with `height` reserves its box before the file arrives |
| `height` | `int\|null` | `null` | The image's height in pixels |
| `phone-src` | `string\|null` | `null` | The file a phone shows below `48rem`. See [A File for the Phone](#a-file-for-the-phone) |
| `phone-srcset` | `string\|null` | `null` | Responsive sources of the phone's file |
| `phone-width` | `int\|null` | `null` | The phone file's width in pixels |
| `phone-height` | `int\|null` | `null` | The phone file's height in pixels |
| `loading` | `string` | `'lazy'` | `'lazy'` or `'eager'`. An image at the top of the page loads `eager` |
| `fetchpriority` | `string\|null` | `null` | `'high'`, `'low'` or `'auto'` on the `<img>`; `null` leaves it to the browser. `'high'` suits an image at the top of the page, which the browser then fetches first |
| `markers` | `array` | `[]` | The markers. See [Markers](#markers) |
| `icon` | `string` | `'plus'` | The icon each marker shows |
| `label` | `string\|null` | "Hotspots" | The name of the markers as a group |
| `strip` | `bool` | `true` | The strip of targets under the image on the phone |
| `strip-label` | `string\|null` | "In this image" | The name of the strip |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## Events

| Event | Detail | When |
| --- | --- | --- |
| `wirekit:image-hotspots-select` | `{ index, target, label }` | A marker was pressed, with the mouse, a finger or the keyboard |

## Accessibility

- Every marker is a `<button>` whose name is its target's name, and the markers are a list named by `label`, so a screen reader says how many there are.
- The name beside a marker shows on hover and on focus, never on hover alone. Escape hides every name on screen, the one under the pointer as well while the focus is elsewhere, until its marker loses the focus or the pointer (WCAG 1.4.13). In a modal or a drawer, that press leaves the dialog open, and the next Escape closes it.
- A marker measures `--size-wk-sm`, 2rem, past the 24 pixels of WCAG 2.5.8. It stands on a fill of its own inside a ring in the text color, so one of the two keeps 3:1 against any image (WCAG 1.4.11).
- The strip is a named scroll region the keyboard can reach, and its cards are a list of links.
- The label's fade follows `--transition-wk-duration` and stops under `prefers-reduced-motion: reduce`, and the strip scrolls without a glide there.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Moves to the next marker, in the order of `markers` |
| `Enter` / `Space` | Presses the marker |
| `Escape` | Hides the name beside the focused marker and the one under the pointer. With no name on screen, it closes the dialog around the image |
