npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xrgallery/viewer

v1.1.0

Published

WebXR 360° VR viewer for self-hosting immersive tours

Readme

@xrgallery/viewer

A standalone, self-hostable WebXR viewer for immersive 360° virtual tours. Built on Babylon.js.

This is the open-source viewer component of XRGallery. Use it to embed and self-host tours on your own site. To create tours visually with a drag-and-drop editor, AI generation, and cloud hosting, use the XRGallery platform.

Features

  • 360° panoramas (images, video, HLS streaming, solid colors)
  • WebXR/VR headset support (Quest, Vive, Index, etc.)
  • Interactive hotspots (info panels, navigation portals, audio, lead capture)
  • 3D model loading (glTF/GLB)
  • Multi-stage tours with portal transitions
  • Floor plan and geographic map navigation
  • Spatial audio
  • Post-processing effects (bloom, DOF, film grain)
  • Mobile touch controls
  • Zero dependencies beyond the bundle (Babylon.js is included)

Install

npm:

npm install @xrgallery/viewer

CDN:

<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>

Quick Start

Scene ID (simplest — loads from XRGallery cloud)

<div id="app" style="width: 100%; height: 100vh;"></div>
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
<script>
  (async () => {
    const viewer = await XRGallery.create({
      element: '#app',
      sceneId: 'YOUR_SCENE_ID'
    });
  })();
</script>

Inline Config (self-hosted, no cloud dependency)

<div id="app" style="width: 100%; height: 100vh;"></div>
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
<script>
  (async () => {
    const viewer = await XRGallery.create({
      element: '#app',
      config: {
        experience: { title: "My Tour" },
        stages: [
          {
            id: "lobby",
            name: "Welcome",
            skybox: { type: "image", url: "https://example.com/panorama.jpg" }
          }
        ]
      }
    });
  })();
</script>

React / TypeScript

import { useEffect, useRef } from 'react';

function VRViewer({ sceneId }: { sceneId: string }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const viewer = XRGallery.create({ element: ref.current, sceneId });
    return () => { viewer.then(v => v.dispose()); };
  }, [sceneId]);

  return <div ref={ref} style={{ width: '100%', height: '100%' }} />;
}

Vanilla JS with Config

const viewer = await XRGallery.create({
  element: document.getElementById('viewer-container'),
  config: {
    experience: { title: "Gallery Tour" },
    stages: [
      {
        id: "main",
        name: "Main Hall",
        skybox: { type: "image", url: "/panorama.jpg" },
        hotspots: [
          {
            id: "info-1",
            type: "info",
            position: { x: 0, y: 1.6, z: -5 },
            label: "Welcome",
            infoTitle: "Hello!",
            infoDescription: "Look around by dragging."
          }
        ]
      }
    ]
  }
});

// Later: clean up
viewer.dispose();

API Reference

XRGallery.create(options): Promise<ViewerInstance>

| Option | Type | Description | |--------|------|-------------| | element | string \| HTMLElement \| null | CSS selector, DOM element, or React ref. Omit to use document.body. | | sceneId | string | Scene ID to load from XRGallery cloud. | | config | XRGalleryConfig | Inline config object (see below). | | firebaseConfig | FirebaseConfig | Override Firebase config (advanced, only with sceneId). | | title | string | Custom page title. |

Provide either sceneId or config, not both.

ViewerInstance

| Method | Description | |--------|-------------| | dispose() | Stops rendering and releases GPU/audio resources. | | isDisposed() | Returns true if dispose() has been called. |

Configuration Reference

Top-Level Config

{
  experience: { ... },       // Required: title, description, settings
  stages: [ ... ],           // Required: array of tour locations
  navigation: { ... },       // Optional: floor plans or maps
  globalAudio: { ... }       // Optional: background music across all stages
}

Experience

experience: {
  title: "Museum Tour",           // Required
  description: "Virtual museum",  // Optional
  defaultFOV: 1.0                 // Camera field of view (0.3-2.5, default: 1.0)
}

Stage

Each stage is a location in the tour:

{
  id: "gallery-1",                // Unique ID (required)
  name: "Main Gallery",           // Display name (required)
  skybox: { ... },                // 360 background (required)
  hotspots: [ ... ],              // Interactive elements (optional)
  models: [ ... ],                // 3D models (optional)
  planes: [ ... ],                // 2D images/videos in 3D space (optional)
  lights: [ ... ],                // Custom lighting (optional)
  audioUrl: "https://example.com/music.mp3",  // Per-stage ambient audio URL (optional)
  audioVolume: 0.5,                           // 0-1 (optional)
  initialCameraTarget: { x: 0, y: 0, z: -100 }  // Where camera looks first
}

Skybox Types

Image (equirectangular panorama):

skybox: { type: "image", url: "https://example.com/panorama.jpg", rotation: 90 }

Video (360 video or HLS stream):

skybox: {
  type: "video",
  url: "https://example.com/360-video.mp4",
  // Or HLS:
  hlsUrl: "https://stream.mux.com/xxx.m3u8",
  thumbnailUrl: "https://example.com/preview.jpg",
  autoplay: true,
  loop: true
}

Solid color:

skybox: { type: "color", color: "#1a1a2e" }

Hotspots

All hotspots share these base fields:

{
  id: "unique-id",                    // Required
  type: "info" | "navigation" | "audio" | "both" | "lead_capture",
  position: { x: 0, y: 1.6, z: -5 }, // 3D position in meters
  label: "Click Me",                  // Hover text
  color: "#4A90E2"                    // Optional accent color
}

Info hotspot — displays a panel with rich content:

{
  type: "info",
  infoTitle: "About This Artwork",
  infoDescription: "Created in 1920...",
  infoImageUrl: "https://example.com/detail.jpg",
  linkUrl: "https://example.com",                 // Optional link button URL
  linkText: "Website",                            // Optional link button text
  videoUrl: "https://example.com/video.mp4",      // Optional embedded video
  iframeUrl: "https://example.com/embed",         // Optional iframe
  infoContentType: "video"                        // "image" | "video" | "iframe"
}

Navigation hotspot — portal to another stage:

{
  type: "navigation",
  targetStageId: "gallery-2",
  portalEffectStyle: "enhanced",  // "clean" | "enhanced" | "minimal" | "alien" | "floor-arrow"
  portalColor: "#4A90E2",
  portalRestSize: 1.0,
  portalHoverSize: 1.2,
  targetViewDirection: { x: 0, y: 0, z: -1 }  // Camera direction on arrival
}

Audio hotspot — plays audio (optionally spatial/3D):

{
  type: "audio",
  audioUrl: "https://example.com/narration.mp3",
  audioVolume: 0.8,
  audioLoop: false,
  audioSpatial: true,
  audioRolloffFactor: 1,
  audioMaxDistance: 100
}

Combined hotspot — info panel + audio narration:

{
  type: "both",
  infoTitle: "The Starry Night",
  infoDescription: "Vincent van Gogh, 1889...",
  audioUrl: "https://example.com/narration.mp3"
}

Lead capture hotspot — contact/inquiry form:

{
  type: "lead_capture",
  leadForm: {
    formType: "contact",  // "contact" | "email_capture" | "inquiry" | "custom"
    title: "Contact Us",
    description: "We'll get back to you within 24 hours",
    submitButtonText: "Send Message",
    fields: [
      { name: "name", type: "text", label: "Name", required: true },
      { name: "email", type: "email", label: "Email", required: true },
      { name: "message", type: "textarea", label: "Message", required: false }
    ]
  }
}

3D Models

models: [
  {
    id: "sculpture",
    url: "https://example.com/sculpture.glb",
    position: { x: 0, y: 0, z: -5 },
    rotation: { x: 0, y: 1.57, z: 0 },  // Radians
    scale: 2.0  // Or { x: 2, y: 2, z: 2 }
  }
]

2D Planes

Floating images or videos in 3D space:

planes: [
  {
    type: "image",
    url: "https://example.com/poster.jpg",
    position: { x: 0, y: 2, z: -6 },
    rotation: { x: 0, y: 0, z: 0 },
    scale: { width: 3, height: 4 }
  },
  {
    type: "video",
    url: "https://example.com/promo.mp4",
    hlsUrl: "https://stream.mux.com/xyz.m3u8",
    position: { x: 4, y: 1.5, z: -5 },
    scale: { width: 4, height: 2.25 },
    autoplay: true, loop: true, muted: false
  }
]

Custom Lighting

lights: [
  { type: "hemispheric", intensity: 0.7, diffuse: "#FFFFFF", groundColor: "#444444", direction: { x: 0, y: 1, z: 0 } },
  { type: "point", position: { x: 0, y: 5, z: 0 }, intensity: 1.5, diffuse: "#FFE4B5", range: 20 },
  { type: "spot", position: { x: 2, y: 4, z: -3 }, direction: { x: 0, y: -1, z: 0 }, intensity: 2, angle: 0.8, exponent: 2 }
]

Navigation (Floor Plans & Maps)

Floor plan:

navigation: {
  type: "floorplan",
  showMinimap: true,
  floorPlans: [
    { floor: 1, name: "Ground Floor", imageUrl: "https://example.com/floor1.png" }
  ],
  markers: [
    { stageId: "lobby", label: "Lobby", floorPlanPosition: { x: 0.5, y: 0.3 } }
  ]
}

Geographic map:

navigation: {
  type: "map",
  showMinimap: true,
  map: { style: "streets", defaultZoom: 15 },
  markers: [
    { stageId: "entrance", label: "Main Entrance", geoPosition: { lat: 40.7128, lng: -74.0060 } }
  ]
}

Global Audio

Background music that plays across all stages:

globalAudio: {
  url: "https://example.com/background-music.mp3",
  volume: 0.3,
  loop: true
}

Positioning Guide

Positions use 3D coordinates in meters from the camera center:

| Axis | Negative | Positive | |------|----------|----------| | x | Left | Right | | y | Down | Up | | z | In front | Behind |

Rules of thumb:

  • Navigation portals: y: 0 (floor level)
  • Info hotspots: y: 1.5 to y: 1.8 (eye level)
  • Comfortable distance: z: -3 to z: -6

Migrating from v1.0

v1.1 is fully backward compatible. The window.__XRGALLERY_CONFIG__ approach still works. To adopt the new API:

Before (v1.0):

<script>
  window.__XRGALLERY_CONFIG__ = {
    experience: { title: "My Tour" },
    stages: [...]
  };
</script>
<script src="viewer-bundle.iife.js"></script>

After (v1.1):

<div id="app"></div>
<script src="viewer-bundle.iife.js"></script>
<script>
  (async () => {
    const viewer = await XRGallery.create({
      element: '#app',
      config: {
        experience: { title: "My Tour" },
        stages: [...]
      }
    });
  })();
</script>

Benefits of the new API:

  • No global variable pollution
  • Works with React refs and any framework
  • Returns a ViewerInstance with dispose() for cleanup
  • sceneId mode fetches config from XRGallery cloud automatically

Browser Support

| Browser | Version | WebXR | |---------|---------|-------| | Chrome | 79+ | Full | | Firefox | 79+ | Full | | Safari | 15.4+ | Partial | | Edge | 79+ | Full |

VR Headset Support

Any WebXR-compatible device: Meta Quest 2/3/Pro, HTC Vive, Valve Index, Windows Mixed Reality, Pico 4.

What This Package Is (and Isn't)

This package is: A self-contained viewer for rendering 360° tours in the browser. You provide the config JSON and media URLs, it renders the experience. MIT licensed, no server required, no tracking, no external calls.

This package is not: The XRGallery editor, dashboard, or cloud platform. Those are proprietary SaaS features available at xrgallery.online. If you want a visual editor, AI scene generation, cloud hosting, analytics, or team collaboration, sign up there.

Creating Tours

You have two options:

  1. Write config JSON by hand using this package (free, self-hosted)
  2. Use the XRGallery editor at xrgallery.online to build tours visually, then export them to self-host or use cloud hosting

License

MIT