<bmx-panorama-viewer>
A 360-degree picture you look around in, and tours of several joined by hotspots: estate agents' and hotels' virtual tours, venues, construction progress, museums, training sites, campus visits. Drag, swipe, use the keys or move the phone to look around; zoom with the wheel, a pinch or the keys; hotspots go to the next scene (with a short move through), show a note or open a link; a floor plan shows where you stand and which way you face, and a strip lists the scenes. Pictures that do not cover the whole sphere (a 180-degree panorama, say) are kept within their edges.
13 properties · 6 events · 7 methods · 5 parts
Example
Drag to look around, or focus the view and use the arrow keys. The glowing hotspots walk you through the house; the others show notes. The floor plan shows where you stand and which way you face.
Show markup
<bmx-panorama-viewer id="ex-pano" style="--bmx-panorama-viewer-height: 34rem"></bmx-panorama-viewer>
<p class="note" id="ex-pano-out" role="status" style="margin: 12px 0 0">Drag to look around, or focus the view and use the arrow keys. The glowing hotspots walk you through the house; the others show notes. The floor plan shows where you stand and which way you face.</p>
<label style="display: inline-flex; gap: 8px; align-items: center; margin-block-start: 8px"><input type="checkbox" id="ex-pano-edit" /> Edit the tour (double-click to add a hotspot, drag one to move it)</label>
<script type="module">
await customElements.whenDefined('bmx-panorama-viewer');
const viewer = document.getElementById('ex-pano');
const out = document.getElementById('ex-pano-out');
const base = 'assets/panorama/';
const scene = (id, title, view, mapAt, hotspots, caption) => ({
id,
title,
caption,
src: `${base}${id}.jpg`,
preview: `${base}${id}-preview.jpg`,
thumb: `${base}${id}-thumb.jpg`,
north: 0,
...view,
map: mapAt,
hotspots,
});
// A fictional show home: a lounge, a kitchen through the archway, and the garden through the glass doors.
// Hotspot directions are yaw (degrees clockwise from north, which is the middle of each picture) and pitch.
viewer.tour = {
title: '14 Orchard Lane',
start: 'lounge',
map: { src: `${base}plan.svg`, width: 400, height: 420, title: 'Ground floor and garden' },
scenes: [
scene('lounge', 'Lounge', { yaw: 20, pitch: -4, fov: 100 }, { x: 160, y: 356 }, [
{ id: 'to-kitchen', target: 'kitchen', yaw: 71.6, pitch: -9.9, title: 'Kitchen', targetYaw: 90 },
{ id: 'to-garden', target: 'garden', yaw: 0, pitch: -11, title: 'Garden', targetYaw: 160 },
{ id: 'shelves', yaw: -52.6, pitch: -7.8, title: 'Built-in shelving', text: 'Solid oak, floor to ceiling, with concealed lighting on each shelf.' },
{ id: 'art', yaw: 174.8, pitch: 1.3, title: 'Artwork', text: 'Shown for styling; not included in the sale.' },
{ id: 'spec', yaw: -84.3, pitch: 0, title: 'Window specification', href: 'https://example.com/orchard-lane/specification' },
], 'The lounge, with glass doors to the garden.'),
scene('kitchen', 'Kitchen', { yaw: 90, pitch: -6, fov: 100 }, { x: 260, y: 340 }, [
{ id: 'to-lounge', target: 'lounge', yaw: -84.3, pitch: -15.3, title: 'Lounge', targetYaw: 200 },
{ id: 'island', yaw: 180, pitch: -29.8, title: 'Quartz island', text: 'Seats four, with power sockets set into the end panel.' },
{ id: 'tiles', yaw: 90, pitch: -9.9, title: 'Hand-glazed tiles', text: 'Splashback in a sea-green glaze, made locally.' },
]),
scene('garden', 'Garden', { yaw: 160, pitch: -2, fov: 100 }, { x: 160, y: 190 }, [
{ id: 'to-lounge-2', target: 'lounge', yaw: 180, pitch: -6.2, title: 'Back inside', targetYaw: 0 },
{ id: 'tree', yaw: -39.3, pitch: 7.2, title: 'Mature oak', text: 'About forty years old; protected, so it stays.' },
{ id: 'patio', yaw: 134.3, pitch: -16, title: 'Sandstone patio', text: 'About 30 square metres, laid in 2024.' },
]),
],
};
viewer.addEventListener('bmxSceneChange', e => (out.textContent = `bmxSceneChange: ${e.detail.previous ?? '-'} → ${e.detail.scene}.`));
viewer.addEventListener('bmxHotspotClick', e => (out.textContent = `bmxHotspotClick: ${e.detail.hotspot.title} (${e.detail.hotspot.type}).`));
viewer.addEventListener('bmxChange', e => (out.textContent = `bmxChange: ${e.detail.reason} ${e.detail.hotspot} in ${e.detail.scene}. Save e.detail.tour to keep it.`));
document.getElementById('ex-pano-edit').addEventListener('change', e => (viewer.editable = e.target.checked));
</script>
<bmx-panorama-viewer src="harbour-360.jpg"></bmx-panorama-viewer>
<bmx-panorama-viewer id="tour"></bmx-panorama-viewer>
<script>
tour.tour = { start: 'hall', scenes: [{ id: 'hall', src: 'hall.jpg', hotspots: [{ target: 'kitchen', yaw: 80, pitch: -10, title: 'Kitchen' }] }, ...] };
</script>
Drawn with WebGL from the picture itself (no sphere mesh, no library); a
small preview shows first while the full picture loads. With editable
the page can author a tour: double-click to add a hotspot, drag to move it,
edit its title, note, type and target, and receive the whole tour in
bmxChange.
Accessibility: the view is focusable and turned with the arrow keys, and the direction faced is announced; every hotspot is a button in the tab order (clockwise from the start), and focusing one turns the view to it; the floor plan's scenes, the scene strip and every tool are labelled buttons.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
autoRotate |
auto-rotate |
number |
0 |
Degrees a second the view turns by itself when left alone; 0 never. Off when the reader prefers reduced motion, unless they turn it on. |
autoRotateDelay |
auto-rotate-delay |
number |
4 |
Seconds of stillness before turning by itself starts again. |
controls |
controls |
boolean |
true |
Show the tools. |
editable |
editable |
boolean |
false |
Hotspots can be added, moved, edited and deleted; changes arrive in bmxChange. |
label |
label |
string |
— | The viewer's accessible name. Default: the tour's title, else "360° view". |
maxFov |
max-fov |
number |
120 |
|
minFov |
min-fov |
number |
30 |
The narrowest and widest the view zooms, in degrees. |
scene |
scene |
string |
'' |
The scene showing. Mutable. |
showMap |
show-map |
boolean |
true |
Show the floor plan (when the tour has one). Mutable. |
showScenes |
show-scenes |
boolean |
true |
Show the strip of scenes (when there is more than one). Mutable. |
src |
src |
string |
— | One equirectangular picture: the quick way to show a single panorama. |
strings |
strings |
Partial<BmxPanoramaViewerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
tour |
tour |
BmxPanoTour | Record<string, unknown> | unknown[] | string |
— | A tour: { title, start, scenes, map }, an array of scenes, or JSON of either. Each scene: id, title, src, preview, thumb, yaw, pitch, fov, north, haov, vaov, vOffset, hotspots, map: { x, y }. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxPanoChangeDetail |
Fired when an editable tour changes, with the whole tour. |
bmxError |
BmxPanoLoadDetail |
Fired when a scene's picture cannot be loaded. |
bmxHotspotClick |
BmxPanoHotspotDetail |
Fired when a hotspot is chosen. Cancel to handle it yourself. |
bmxLoad |
BmxPanoLoadDetail |
Fired when a scene's full picture has loaded. |
bmxSceneChange |
BmxPanoSceneChangeDetail |
Fired when another scene shows. |
bmxViewChange |
BmxPanoViewChangeDetail |
Fired as the view turns or zooms, at most five times a second, and when it comes to rest. |
Methods
| Method | Signature | Description |
|---|---|---|
capture |
capture(type?: string, quality?: number) => Promise<Blob | null> |
A picture of the current view (PNG unless another type is asked for). |
getTour |
getTour() => Promise<BmxPanoTour> |
The tour as it is now (with any edits). |
getView |
getView() => Promise<BmxPanoView> |
Where the reader is looking now. |
goToScene |
goToScene(id: string, view?: Partial<BmxPanoView>) => Promise<void> |
Goes to another scene, facing view (else its own starting view). |
lookAt |
lookAt(yaw: number, pitch?: number, fov?: number) => Promise<void> |
Turns to face a yaw and pitch (and zooms to fov, if given). |
setFocus |
setFocus() => Promise<void> |
Puts keyboard focus on the view. |
setView |
setView(view: Partial<BmxPanoView>, animate?: boolean) => Promise<void> |
Turns to a view (animated unless animate is false). |
CSS shadow parts
| Part | Description |
|---|---|
hotspot |
A hotspot; also hotspot-scene, hotspot-info, hotspot-link. |
map |
The floor plan. |
scenes |
The strip of scenes. |
toolbar |
The tools. |
viewport |
The view itself. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-panorama-viewer-accent |
Hotspots, the plan's "you are here" and pressed tools. |
--bmx-panorama-viewer-glass |
The background of the floating controls. |
--bmx-panorama-viewer-height |
The viewer's height. |
--bmx-panorama-viewer-on-glass |
Text and icons on the floating controls. |
--bmx-panorama-viewer-radius |
Corner radius of the viewer. |