Build multi-scene 360° virtual tours with scene-switch hotspots and live camera state

Virtual Tours

Build multi-scene 360° virtual tours with scene-switch hotspots and live camera state


Overview

A tour is a dictionary of scenes plus a default block that picks the first scene. Each scene is a Pannellum scene configuration: an equirectangular panorama URL, an initial camera (pitch, yaw, hfov), and optional hotSpots.

Hotspots with "type": "scene" switch to another scene when clicked — that's what turns a set of panoramas into a guided tour. sceneFadeDuration crossfades the transition.


Live example

Click the hotspot on the horizon to jump between scenes — and watch currentScene, pitch and yaw stream into the Dash callback below the viewer.

Scene jumps here are a clean crossfade with no "Loading…" flash: since 0.2.0 the component prefetches every other scene's panorama once the viewer is up (preloadScenes, on by default), and this example also sets hideLoadingSpinner=True so the load box never appears even on a cold cache:

# File: docs/tours/tour_example.py

import dash_mantine_components as dmc
from dash import Input, Output, callback, html

from dash_pannellum import DashPannellum

tour = {
    "default": {
        "firstScene": "observatory",
        "sceneFadeDuration": 1000,
        "author": "Pip Install Python",
    },
    "scenes": {
        "observatory": {
            "title": "ALMA Observatory",
            "hfov": 110,
            "pitch": -3,
            "yaw": 117,
            "type": "equirectangular",
            "panorama": "https://pannellum.org/images/alma.jpg",
            "autoLoad": True,
            "hotSpots": [
                {
                    "pitch": -2.1,
                    "yaw": 132.9,
                    "type": "scene",
                    "text": "Travel to Cerro Toco",
                    "sceneId": "desert",
                }
            ],
        },
        "desert": {
            "title": "Cerro Toco",
            "hfov": 110,
            "yaw": 5,
            "type": "equirectangular",
            "panorama": "https://pannellum.org/images/cerro-toco-0.jpg",
            "hotSpots": [
                {
                    "pitch": -0.6,
                    "yaw": 37.1,
                    "type": "scene",
                    "text": "Back to the observatory",
                    "sceneId": "observatory",
                    "targetYaw": -23,
                    "targetPitch": 2,
                }
            ],
        },
    },
}

component = html.Div(
    [
        DashPannellum(
            id="tour-demo",
            tour=tour,
            autoLoad=True,
            compass=True,
            # 0.2.0: scenes are prefetched (preloadScenes defaults True) and
            # the load box is suppressed, so hotspot jumps are a clean fade.
            hideLoadingSpinner=True,
            width="100%",
            height="450px",
        ),
        dmc.Group(
            [
                dmc.Badge("—", id="tour-demo-scene", size="lg", variant="light"),
                dmc.Code("waiting for camera…", id="tour-demo-view"),
            ],
            mt="md",
            gap="md",
        ),
    ]
)


@callback(
    Output("tour-demo-scene", "children"),
    Output("tour-demo-view", "children"),
    Input("tour-demo", "currentScene"),
    Input("tour-demo", "pitch"),
    Input("tour-demo", "yaw"),
)
def show_camera_state(scene, pitch, yaw):
    view = f"pitch {pitch or 0:+.1f}° · yaw {yaw or 0:+.1f}°"
    return scene or "—", view

:defaultExpanded: false :withExpandedButton: true


Scene configuration

The most useful per-scene keys (all standard Pannellum options):

KeyTypeDescription
panoramastrURL of the equirectangular image
titlestrShown in the viewer's title bar
hfovnumberInitial horizontal field of view, degrees
pitch / yawnumberInitial camera orientation
autoLoadboolLoad this scene without a click
hotSpotslistScene-switch and info hotspots

A scene-switch hotspot:

{
    "pitch": -2.1,          # where the hotspot sits in the panorama
    "yaw": 132.9,
    "type": "scene",        # makes it a navigation hotspot
    "text": "Next room",    # tooltip
    "sceneId": "kitchen",   # scene to load on click
    "targetYaw": -23,       # optional: camera orientation after the jump
    "targetPitch": 2,
}

Set showCenterDot=True and read pitch/yaw from a callback while aiming the dot at your target — the Callback Hotspots page demonstrates this workflow.


Tracking the active scene

currentScene updates whenever the user navigates, so you can drive any output from tour progress — breadcrumbs, maps, info panels:

@callback(
    Output("scene-info", "children"),
    Input("tour-demo", "currentScene"),
)
def describe(scene):
    return SCENE_DESCRIPTIONS.get(scene, "")

Source: /components/tours

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: