Navigation

v0.0.4
Media

Hotspot

Points on a picture that say what is there.

On this page

A hotspot puts points on a picture that say what is there: the parts of a product, the places on a plan, the things worth noticing in a screenshot. The hotspot is the frame around the picture, and each pin is a button placed on it in percent. A pin opens what a button opens, so a popover needs no script.

Hotspot

<figure class="figure hotspot w-full max-w(480)">  <img class="figure-media" src="/demo/moonstone.svg" alt="A pale stone hanging over two ranges of grey hills" style="--figure-ratio: 16 / 9">  <button class="hotspot-pin" type="button" style="--x: 72%; --y: 32%" popovertarget="spot-stone" aria-label="The stone">1</button>  <button class="hotspot-pin" type="button" style="--x: 30%; --y: 62%" popovertarget="spot-hills" aria-label="The far hills">2</button>  <div class="popover" id="spot-stone" popover>    <p class="m(0) fg-text-strong font-medium">The stone</p>    <p class="m(0) fg-text-muted">Moonstone, lit from above and slightly left, as every surface in the theme is.</p>  </div>  <div class="popover" id="spot-hills" popover>    <p class="m(0) fg-text-strong font-medium">The far hills</p>    <p class="m(0) fg-text-muted">One step of the same grey lighter than the near ones.</p>  </div></figure>
Press a pin.

#Placing a pin

hotspot makes the element around the picture as wide as the picture and the reference for its pins. Each hotspot-pin sets --x and --y in its style, in percent of the picture's width and height from its top left corner, and the pin's middle stands on that point. Percent keeps a pin on its place at any size of the picture.

A pin is a <button>, or a link when it goes somewhere. With popovertarget it opens a popover, which the browser places against the pin where anchor positioning is supported and in the middle of the viewport elsewhere. With interestfor it shows a tooltip on hover and focus.

#The pin

A pin is a 24px disc of ink with a white rim and a dark shade outside the rim. The rim and the shade are the same in both themes, since a picture has no theme: between them a pin is found on a light picture and on a dark one. It may hold a number, which ties it to a list of the same points in the text, or nothing.

A ring leaves the pin every few seconds, to say it can be pressed. With reduced motion the ring rests. On a touch screen the pin keeps its size and its target grows to 44px.

Without numbers

<figure class="figure hotspot w-full max-w(360)">  <img class="figure-media" src="/demo/moonstone.svg" alt="A pale stone hanging over two ranges of grey hills" style="--figure-ratio: 16 / 9">  <button class="hotspot-pin" type="button" style="--x: 72%; --y: 32%" popovertarget="plain-stone" aria-label="The stone"></button>  <button class="hotspot-pin" type="button" style="--x: 22%; --y: 78%" popovertarget="plain-hills" aria-label="The near hills"></button>  <div class="popover" id="plain-stone" popover>The stone the light theme is named for.</div>  <div class="popover" id="plain-hills" popover>The near hills, in the ink of the page.</div></figure>

#Accessibility

  • Every pin needs an accessible name that says what it points at: aria-label="The arm", not "Pin 1". A number inside the pin is not a name.
  • Pins are reached with Tab in the order they are written, so write them in the order a reader would look, not in the order they were added.
  • The picture's alt describes the whole picture. What the pins say must also be available without them: list the same points under the picture, numbered the same way.
  • Keep pins at least 44px apart on a picture that may be seen on a phone, so two targets never overlap.

#Classes

ClassDescription
hotspotThe frame around a picture, as wide as the picture: pins are placed against it
hotspot-pinOne pin, on a button with an accessible name: a 24px disc of ink with a white rim, whose middle stands at --x and --y. It may hold a number. A ring leaves it now and then, and rests while the pin is under the pointer or has focus

#Custom properties

PropertyDefaultDescription
--xThe pin's place across the picture, in percent from its left edge. On hotspot-pin
--yThe pin's place down the picture, in percent from its top edge. On hotspot-pin

#Source

@master/ui/components/hotspot.css
/* * Hotspot: points on a picture that say what is there. * The hotspot is the frame around a picture; each pin is a button placed at --x and --y of it, in percent. * A pin opens whatever its button opens: a popover with popovertarget, a tooltip with interestfor. * A pin is a small disc of ink with a white rim and a dark shade outside the rim, so it reads on any * picture in either theme. It may hold a number. A ring leaves it now and then, to say it can be pressed; * with reduced motion the ring rests. * Geometry: a 24px disc, and a 44px target on a touch screen. */@layer components {  .hotspot {    position: relative;    display: block;    width: fit-content;    max-width: 100%;    min-width: 0;    margin: 0;  }  /* One pin: its middle stands on the point. */  .hotspot-pin {    --hotspot-size: 1.5rem;    position: absolute;    top: var(--y, 50%);    left: var(--x, 50%);    display: grid;    place-items: center;    width: var(--hotspot-size);    height: var(--hotspot-size);    margin: 0;    padding: 0;    border: 0;    border-radius: 999px;    background: var(--color-surface-inverse);    color: var(--color-text-inverse);    box-shadow: 0 0 0 1.5px var(--color-on-image), 0 0 0 2px var(--color-on-image-shade), 0 2px 6px 1px var(--color-on-image-shade);    font: var(--font-weight-control) .6875rem/1 var(--font-family-sans);    font-feature-settings: inherit;    font-variant-numeric: tabular-nums;    translate: -50% -50%;    appearance: none;    cursor: pointer;    touch-action: manipulation;    user-select: none;    -webkit-user-select: none;    -webkit-tap-highlight-color: transparent;    transition: scale var(--duration-quick) var(--easing-glide);    /* The ring that leaves the pin. */    &::before {      content: '';      position: absolute;      inset: 0;      border-radius: inherit;      box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-on-image) 70%, transparent);      pointer-events: none;      animation: hotspot-ring 2.4s var(--easing-glide) infinite;    }    /* The target a finger needs, around the disc. */    &::after {      content: '';      position: absolute;      inset: 0;      @media (pointer: coarse) {        inset: calc((var(--hotspot-size) - 2.75rem) / 2);      }    }    @media (hover: hover) {      &:hover {        scale: 1.12;      }    }    /* Found: the ring that asks to be noticed rests while the pin is under the pointer or has focus. */    &:is(:hover, :focus-visible)::before {      animation: none;    }    &:active {      scale: .94;      transition-duration: var(--duration-press);    }    &:focus-visible {      outline: 2px solid var(--color-on-image);      outline-offset: 3px;      box-shadow: 0 0 0 1.5px var(--color-on-image), 0 0 0 3px var(--color-focus), 0 0 0 7px var(--color-on-image-shade);    }    @media (prefers-reduced-motion: reduce) {      transition: none;      &::before {        animation: none;      }      &:hover, &:active {        scale: none;      }    }    @media (forced-colors: active) {      outline: 2px solid ButtonText;      outline-offset: -2px;      background: ButtonFace;      color: ButtonText;      &::before {        animation: none;      }    }  }}@keyframes hotspot-ring {  0%, 20% {    box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-on-image) 70%, transparent);  }  70%, 100% {    box-shadow: 0 0 0 .75rem transparent;  }}