---
title: Image Hotspots Editor
description: Points on an image for image-hotspots, set with a click or a button, moved by dragging, the arrow keys or number fields, and ordered in a list.
visibility: guest
draft: false
---

# Image Hotspots Editor

`<x-wirekit::image-hotspots-editor>` is where the points of an [image with hotspots](/components/image-hotspots) are made. A click on the image sets a point, dragging moves it, and the list beside the image holds every point with a number field for X and Y, in the order Tab later reaches the markers. Its value is the list `<x-wirekit::image-hotspots>` takes as `markers`, so what the editor writes is what the page shows.

## Basic Usage

Give the image, its size in pixels and the points to start with:

:::preview{title="Two Points on a Room"}
<x-wirekit::image-hotspots-editor
    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"
    :value="[
        ['x' => 24, 'y' => 58, 'label' => 'Sofa Bergen'],
        ['x' => 71, 'y' => 34, 'label' => 'Floor lamp Arc'],
    ]"
/>
:::

A point without a `label` is named after its place in the list, "Point 3". The editor carries every key of a point along as it is, a `label`, a `target` or anything else the page keeps on it, and writes only the position.

## With Livewire

Bind the list with `wire:model`. The property receives the whole list after every change, and the same array is what [image hotspots](/components/image-hotspots#markers) take. Here a `Look` keeps its points in a JSON column cast to `array`:

```php
<?php

namespace App\Livewire;

use App\Models\Look;
use Illuminate\Contracts\View\View;
use Livewire\Component;

class LookEditor extends Component
{
    public Look $look;

    /** @var list<array<string, mixed>> */
    public array $hotspots = [];

    public function mount(Look $look): void
    {
        $this->look = $look;
        $this->hotspots = $look->hotspots ?? [];
    }

    public function save(): void
    {
        $this->validate([
            'hotspots.*.x' => ['required', 'numeric', 'between:0,100'],
            'hotspots.*.y' => ['required', 'numeric', 'between:0,100'],
        ]);

        $this->look->update(['hotspots' => $this->hotspots]);
    }

    public function render(): View
    {
        return view('livewire.look-editor');
    }
}
```

```blade
<div>
    <x-wirekit::image-hotspots-editor
        :src="$look->image_url"
        :alt="$look->image_alt"
        width="1600"
        height="1000"
        wire:model="hotspots"
    />

    <x-wirekit::button wire:click="save">Save</x-wirekit::button>
</div>
```

With `wire:model` the list waits for the next request, here the one Save sends. `wire:model.live` sends every change at once: each point set or deleted, each press of an arrow key, and a drag once it is let go, not during it. `wire:model.live.change` sends when a point is committed in the same way, and `wire:model.live.blur` when the reader leaves the editor.

In an Alpine component of your own, `x-model` binds the same list. See [An `x-model` on a WireKit component](/getting-started/integration#an-x-model-on-a-wirekit-component).

## A File for the Phone

Where the page shows another file on the phone, give it as `phone-src` with its size. A switch above the image then edits either file, and every point takes a position of its own on the phone's, `phoneX` and `phoneY`. A point that was never moved on the phone stands where the desktop puts it:

:::preview{title="A Desktop and a Phone File"}
<x-wirekit::image-hotspots-editor
    src="/placeholder/1600x1000?bg=e2e8f0&fg=334155&label=Desktop&gradient=none"
    alt="A living room with a sofa, a floor lamp and a rug"
    width="1600"
    height="1000"
    phone-src="/placeholder/900x1200?bg=fde68a&fg=78350f&label=Phone&gradient=none"
    phone-width="900"
    phone-height="1200"
    :value="[
        ['x' => 24, 'y' => 58, 'label' => 'Sofa Bergen'],
        ['x' => 71, 'y' => 34, 'phoneX' => 40, 'phoneY' => 20, 'label' => 'Floor lamp Arc'],
    ]"
/>
:::

## The Fields of a Point

What a point leads to is the page's own question: a product, a category, a hint. The editor tells the page which point the reader is on, and the page shows its fields for that point. When the reader selects a point, sets one or adds one, the editor sends `wirekit:image-hotspots-editor-select`, and it bubbles. Its `detail` carries `index` and `point`, a copy of the point:

```blade
<x-wirekit::image-hotspots-editor
    :src="$look->image_url"
    :alt="$look->image_alt"
    width="1600"
    height="1000"
    wire:model="hotspots"
    x-on:wirekit:image-hotspots-editor-select="$wire.set('editing', $event.detail.index)"
/>

@if ($editing !== null && isset($hotspots[$editing]))
    <x-wirekit::input label="Name of the point" wire:model="hotspots.{{ $editing }}.label" />
@endif
```

The component above needs a `public ?int $editing = null;` for this. The field and the editor are bound to the same list, so the field writes the name of the point the editor shows.

## Points

Each point is an array, the same shape as the [markers of image hotspots](/components/image-hotspots#markers):

| Key | Type | Description |
| --- | --- | --- |
| `x`, `y` | `int\|float` | The position on the desktop file, in percent of the image's width and height from its top left corner. The editor writes them with one decimal, between 0 and 100 |
| `phoneX`, `phoneY` | `int\|float` | The position on the phone's file. The editor writes them once a point is moved on the phone, and a new point set on the phone gets both pairs |
| `label` | `string` | The point's name, in the list and as the name of its marker. A new point gets an empty one |
| any other key | | Carried along as it is: a `target`, an `href`, an `image` |

## 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 |
| `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 phone's file. See [A File for the Phone](#a-file-for-the-phone) |
| `phone-width` | `int\|null` | `null` | The phone file's width in pixels |
| `phone-height` | `int\|null` | `null` | The phone file's height in pixels |
| `value` | `array` | `[]` | The points to start with where no binding hands them over. See [Points](#points) |
| `label` | `string\|null` | "Points" | The name of the list of points |
| `help` | `string\|null` | "Click the image to set a point. Drag a point or use the arrow keys to move it." | The sentence above the image, as text or as markup, which every marker reads as its description |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## Events

| Event | Detail | When |
| --- | --- | --- |
| `wirekit:image-hotspots-editor-select` | `{ index, point }` | The reader selected a point, set one with a click or added one with the button |

## Accessibility

- Dragging is never the only way to place a point (WCAG 2.5.7). A selected marker moves with the arrow keys, and every row of the list has a number field for X and Y.
- A point can be set without a pointer (WCAG 2.1.1): "Add a point" sets one in the middle of the image, and the focus goes to its marker, so the arrow keys move it at once.
- Every marker is a `<button>` named after its point, pressed while its point is selected, and described by the sentence above the image.
- The list is named by `label`, each row starts with the number of its point, and each of its buttons and fields is named after the point: "Move Sofa Bergen up", "Sofa Bergen, X in percent".
- A status region reads what changed: the position after a move, a point added, a point deleted, the new place of a row.
- The focus stays with the reader through every change that takes its element away. A deleted point hands it to the point that takes its place, a row moved up or down keeps it on its arrow, and a list that is left empty hands it to "Add a point".
- A marker measures `--size-wk-sm`, 2rem, and takes a 44 pixel area on a touch screen. The buttons of a row grow to 44 pixels there.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Moves between the markers, then through the rows of the list and "Add a point" |
| `Enter` / `Space` | On a marker or a name in the list, selects the point |
| `←` `→` `↑` `↓` | On a marker, moves the point by 1 % |
| `Shift` + arrow | On a marker, moves the point by 10 % |
| `Delete` / `Backspace` | On a marker, deletes the point |
