Skip to content

Panorama

Panorama is exported from a subpath (not the main barrel) so it isn’t bundled into apps that don’t use it:

import Panorama from '@delightstack/components/panorama';
import type { PanoramaHotspot } from '@delightstack/components';

Panorama requires three as a peer dependency (loaded lazily at runtime):

Terminal window
pnpm add three
View code
<script>
import Panorama from '@delightstack/components/panorama';
</script>
<Panorama src="/panoramas/mountain-360.jpg" />

Enable gentle continuous rotation. Rotation pauses when the user interacts and resumes after 3 seconds of inactivity.

View code
<Panorama src="/panoramas/cityscape.jpg" auto_rotate auto_rotate_speed={1.5} />

Start the viewer facing a specific direction.

<Panorama
src="/panoramas/interior.jpg"
initial_view={{ pitch: 10, yaw: 90 }}
fov={60}
/>

Add clickable markers at specific positions within the panorama.

Click a labelled hotspot.

View code
<script>
import Panorama from '@delightstack/components/panorama';
import type { PanoramaHotspot } from '@delightstack/components';
const hotspots: PanoramaHotspot[] = [
{ position: { pitch: 5, yaw: 45 }, label: 'Living Room' },
{ position: { pitch: -10, yaw: 180 }, label: 'Kitchen' },
{ position: { pitch: 0, yaw: 270 }, label: 'Garden', data: { room_id: 3 } },
];
</script>
<Panorama
src="/panoramas/house.jpg"
{hotspots}
onhotspotclick={({ hotspot }) => console.log('Clicked', hotspot.label)} />

Provide a fallback image for browsers without WebGL support.

<Panorama
src="/panoramas/beach.jpg"
fallback="/panoramas/beach-preview.jpg"
/>

The skeleton is loading-state-driven: it shows while the equirectangular texture is still downloading and dismisses itself the moment the panorama is ready.

View code
<Panorama skeleton src={panorama_url} />
PropTypeDefaultDescription
srcstringrequiredEquirectangular panorama image URL
initial_view{ pitch: number, yaw: number }{ pitch: 0, yaw: 0 }Starting camera orientation in degrees
fovnumber75Field of view in degrees (30-120)
auto_rotatebooleanfalseEnable gentle continuous rotation
auto_rotate_speednumber1Rotation speed multiplier
show_controlsbooleantrueShow zoom, fullscreen, and reset buttons
interactivebooleantrueEnable drag, touch, and scroll interaction
gyroscopebooleanfalseEnable device orientation control on mobile
hotspotsPanoramaHotspot[][]Interactive markers in the panorama
fallbackstringundefinedStatic image URL when WebGL is unavailable
skeletonbooleanfalseShow loading skeleton
idstringautoElement ID
classstring''Additional CSS classes
elementHTMLElementundefinedBindable reference to the root element ($bindable)
EventDetailDescription
onviewchange{ pitch: number, yaw: number, fov: number }Fires when the camera orientation changes (throttled)
onhotspotclick{ hotspot: PanoramaHotspot }Fires when a hotspot is clicked
onloadnoneFires when the panorama is ready
onerror{ error: Error }Fires when loading fails
interface PanoramaHotspot {
position: { pitch: number; yaw: number };
label?: string;
data?: Record<string, unknown>;
}
  • Container has role="application" with aria-label="360 degree panorama viewer"
  • tabindex="0" for keyboard focus
  • Arrow keys pan the view, +/- zoom in/out
  • Home resets the view to the initial position
  • Space toggles auto-rotation pause
  • Hotspot buttons have aria-label set to the hotspot label or “Hotspot N”
  • Control buttons (zoom in, zoom out, fullscreen, reset) have descriptive aria-label
  • Focus-visible outlines on all interactive elements
  • Auto-rotation respects prefers-reduced-motion
  • Pauses rendering when the element is not visible (IntersectionObserver)