Media

ImageHotspots

A picture with numbered points that show a note on hover or focus, with no script: a plan, a product, a photo.

#media#image#popover#accessibility#no-js

Demo

Preview
The flat's floor plan

Rest the pointer on a number, or Tab to it, to read the note.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/image_hotspots.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/image_hotspots.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/image_hotspots.qwe
// A floor plan drawn in SVG, so the demo needs no image files.let image_hotspots_demo_plan = `data:image/svg+xml,${url_encode(    `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" width="800" height="500">`    + `<rect width="800" height="500" fill="#f4efe6"/>`    + `<rect x="40" y="40" width="330" height="260" fill="#fff" stroke="#8a7f70" stroke-width="4"/>`    + `<rect x="370" y="40" width="390" height="180" fill="#fff" stroke="#8a7f70" stroke-width="4"/>`    + `<rect x="370" y="220" width="390" height="240" fill="#fff" stroke="#8a7f70" stroke-width="4"/>`    + `<rect x="40" y="300" width="330" height="160" fill="#fff" stroke="#8a7f70" stroke-width="4"/>`    + `<rect x="60" y="60" width="120" height="80" rx="6" fill="#d6e6f5"/><rect x="400" y="250" width="200" height="110" rx="8" fill="#e8dcc8"/></svg>`)}`let image_hotspots_demo_points = [    { x: 14, y: 20, title: "Bed", text: "A 160 cm bed by the window." },    { x: 60, y: 25, title: "Kitchen", text: "Induction hob, oven, dishwasher." },    { x: 62, y: 62, title: "Living room", text: "A sofa that sleeps two, and the TV." },    { x: 25, y: 76, title: "Bathroom", text: "Walk-in shower; towels provided." },    { x: 90, y: 90, title: "Balcony" },]view ImageHotspotsDemo() =    <div.demo-stack>        <ImageHotspots id="image-hotspots-demo" src={image_hotspots_demo_plan} alt="The flat's floor plan" points={image_hotspots_demo_points}                       label="Rooms"/>        <p.demo-note>Rest the pointer on a number, or Tab to it, to read the note.</p>    </div>

Source

ui/image_hotspots.qwe
// ImageHotspots: a picture with numbered points on it that each show a note// when the pointer rests on them or they take focus: the parts of a product,// the rooms on a plan, who is in a photo. CSS shows the notes, so it needs no// script; each point is a button that screen readers read with its note.////   <ImageHotspots src="/img/bike.jpg" alt="The bike from the side" points={[//       { x: 22, y: 60, title: "Front brake", text: "Hydraulic disc, 180 mm." },//       { x: 71, y: 44, title: "Saddle", text: "Leather, sprung." },//   ]}/>//// src, alt: the picture. points: a list of { x, y, title, text? }: x and y in// percent of the picture's width and height, from the top left; title is the// note's heading, text its body. A note opens below its point in the top part// of the picture and above it elsewhere, and leans left or right near the edges.// id: a prefix for the notes' ids ("ui-image-hotspots"); give each on a page its own.// label: what screen readers call the group of points ("Points of interest").import ui/theme.qwe use ui_clslet image_hotspots_css = css`.ui-image-hotspots { position: relative; display: inline-block; width: fit-content; max-width: 100%; overflow: visible; border-radius: var(--ui-radius); font: 13px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-image-hotspots img { display: block; max-width: 100%; height: auto; border-radius: var(--ui-radius); }.ui-image-hotspots-points { margin: 0; padding: 0; list-style: none; }.ui-image-hotspots-pin-at { position: absolute; width: 0; height: 0; }.ui-image-hotspots-pin {  position: absolute; display: grid; place-items: center; width: 26px; height: 26px; padding: 0; transform: translate(-50%, -50%);  border: 2px solid #fff; border-radius: 50%; background: var(--ui-accent); color: var(--ui-accent-fg);  font: 700 12px/1 var(--ui-font); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ui-accent) 35%, transparent), var(--ui-shadow);  cursor: pointer; transition: transform .15s;}.ui-image-hotspots-pin:hover, .ui-image-hotspots-pin:focus { transform: translate(-50%, -50%) scale(1.15); }.ui-image-hotspots-pin:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--ui-bg), 0 0 0 7px var(--ui-accent); }.ui-image-hotspots-pin-note {  position: absolute; z-index: 2; bottom: 20px; left: 50%; width: max-content; max-width: 240px; padding: 8px 12px;  transform: translateX(-50%); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); text-align: left; visibility: hidden; opacity: 0; transition: opacity .15s;}.ui-image-hotspots-pin-note strong { display: block; font-weight: 600; }.ui-image-hotspots-pin-note span { color: var(--ui-muted); }.ui-image-hotspots-pin-at:hover .ui-image-hotspots-pin-note, .ui-image-hotspots-pin:focus + .ui-image-hotspots-pin-note { visibility: visible; opacity: 1; }.ui-image-hotspots-pin-below .ui-image-hotspots-pin-note { bottom: auto; top: 20px; }.ui-image-hotspots-pin-left .ui-image-hotspots-pin-note { left: auto; right: -14px; transform: none; }.ui-image-hotspots-pin-right .ui-image-hotspots-pin-note { left: -14px; transform: none; }@media (prefers-reduced-motion: reduce) { .ui-image-hotspots-pin, .ui-image-hotspots-pin-note { transition: none; } }`view ImageHotspots(src: str, alt: str = "", points: list = [], id: str = "ui-image-hotspots", label: str = "Points of interest",                   class: str? = null) {    style global image_hotspots_css    <div class={ui_cls(["ui-image-hotspots", class])}>        <img src={src} alt={alt}/>        <ol.ui-image-hotspots-points aria-label={label}>            {for [i, p] in enumerate(points) => <li key={i}                    class={ui_cls(["ui-image-hotspots-pin-at", p.y < 30 && "ui-image-hotspots-pin-below", p.x > 75 && "ui-image-hotspots-pin-left", p.x < 25 && "ui-image-hotspots-pin-right"])}                    style={`left: ${p.x}%; top: ${p.y}%`}>                <button.ui-image-hotspots-pin type="button" aria-label={`${i + 1}: ${p.title}`} aria-describedby={`${id}-${i}`}>{i + 1}</button>                <div.ui-image-hotspots-pin-note id={`${id}-${i}`} role="tooltip">                    <strong>{p.title}</strong>                    {get(p, "text") != null && <span>{get(p, "text")}</span>}                </div>            </li>}        </ol>    </div>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On ImageHotspots: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.