# Virtual Tours

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

**Site index:** [https://pannellum.2plot.dev/llms.txt](https://pannellum.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 13 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://pannellum.2plot.dev/sitemap.xml  


---



### 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:



```python
# 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):

| Key | Type | Description |
|-----|------|-------------|
| `panorama` | str | URL of the equirectangular image |
| `title` | str | Shown in the viewer's title bar |
| `hfov` | number | Initial horizontal field of view, degrees |
| `pitch` / `yaw` | number | Initial camera orientation |
| `autoLoad` | bool | Load this scene without a click |
| `hotSpots` | list | Scene-switch and info hotspots |

A scene-switch hotspot:

```python
{
    "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](/components/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:

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


---

*Source: /components/tours*
