Skip to content

Framework Integration Guides

Examples for using @xrgallery/viewer with popular frameworks and meta-frameworks.

All examples assume you've installed the package:

npm install @xrgallery/viewer

React

import { useEffect, useRef } from 'react';

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

  useEffect(() => {
    const viewerPromise = XRGallery.create({
      element: containerRef.current,
      sceneId
    });

    return () => {
      viewerPromise.then(v => v.dispose());
    };
  }, [sceneId]);

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

With Inline Config

import { useEffect, useRef } from 'react';

const tourConfig = {
  experience: { title: "React Tour" },
  stages: [
    {
      id: "main",
      name: "Main Hall",
      skybox: { type: "image" as const, url: "/panorama.jpg" }
    }
  ]
};

function VRViewer() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const viewerPromise = XRGallery.create({
      element: containerRef.current,
      config: tourConfig
    });

    return () => {
      viewerPromise.then(v => v.dispose());
    };
  }, []);

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

Container sizing

The viewer canvas fills 100% of its container. Make sure the container div has explicit dimensions (height: 100vh, height: 600px, etc.).


Next.js

Since @xrgallery/viewer accesses the DOM, it must run client-side only.

App Router (Next.js 13+)

'use client';

import { useEffect, useRef } from 'react';

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

  useEffect(() => {
    // Dynamic import to avoid SSR issues
    const script = document.createElement('script');
    script.src = 'https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js';
    script.onload = () => {
      const viewerPromise = (window as any).XRGallery.create({
        element: containerRef.current,
        sceneId
      });

      // Store for cleanup
      (containerRef as any).viewerPromise = viewerPromise;
    };
    document.head.appendChild(script);

    return () => {
      const vp = (containerRef as any).viewerPromise;
      if (vp) vp.then((v: any) => v.dispose());
    };
  }, [sceneId]);

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

Pages Router

import dynamic from 'next/dynamic';

const VRViewer = dynamic(() => import('../components/VRViewer'), {
  ssr: false,
  loading: () => <div style={{ height: '100vh', background: '#000' }} />
});

export default function TourPage() {
  return <VRViewer sceneId="abc123" />;
}

No SSR

The viewer requires browser APIs (DOM, WebGL, WebXR). Always disable SSR when using in Next.js.


Vue 3

<template>
  <div ref="container" style="width: 100%; height: 100vh;"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';

const props = defineProps<{ sceneId: string }>();
const container = ref<HTMLDivElement>();

let viewerPromise: Promise<any> | null = null;

onMounted(() => {
  viewerPromise = XRGallery.create({
    element: container.value,
    sceneId: props.sceneId
  });
});

onBeforeUnmount(() => {
  viewerPromise?.then(v => v.dispose());
});
</script>

With Config

<template>
  <div ref="container" style="width: 100%; height: 100vh;"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';

const container = ref<HTMLDivElement>();
let viewerPromise: Promise<any> | null = null;

onMounted(() => {
  viewerPromise = XRGallery.create({
    element: container.value,
    config: {
      experience: { title: "Vue Tour" },
      stages: [
        {
          id: "main",
          name: "Main",
          skybox: { type: "image", url: "/panorama.jpg" }
        }
      ]
    }
  });
});

onBeforeUnmount(() => {
  viewerPromise?.then(v => v.dispose());
});
</script>

Nuxt 3

<!-- components/VRViewer.client.vue -->
<template>
  <div ref="container" style="width: 100%; height: 100vh;"></div>
</template>

<script setup lang="ts">
const props = defineProps<{ sceneId: string }>();
const container = ref<HTMLDivElement>();

let viewerPromise: Promise<any> | null = null;

onMounted(() => {
  viewerPromise = XRGallery.create({
    element: container.value,
    sceneId: props.sceneId
  });
});

onBeforeUnmount(() => {
  viewerPromise?.then(v => v.dispose());
});
</script>

.client.vue suffix

The .client.vue suffix tells Nuxt to only render this component on the client side.


Angular

// vr-viewer.component.ts
import { Component, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';

@Component({
  selector: 'app-vr-viewer',
  template: '<div #container style="width: 100%; height: 100vh;"></div>'
})
export class VRViewerComponent implements OnInit, OnDestroy {
  @Input() sceneId!: string;
  @ViewChild('container', { static: true }) container!: ElementRef<HTMLDivElement>;

  private viewerPromise: Promise<any> | null = null;

  ngOnInit() {
    this.viewerPromise = (window as any).XRGallery.create({
      element: this.container.nativeElement,
      sceneId: this.sceneId
    });
  }

  ngOnDestroy() {
    this.viewerPromise?.then(v => v.dispose());
  }
}

Usage:

<app-vr-viewer [sceneId]="'abc123'"></app-vr-viewer>

Script loading

Add the viewer bundle to your angular.json scripts array or load it via a <script> tag in index.html:

"scripts": [
  "node_modules/@xrgallery/viewer/dist/viewer-bundle.iife.js"
]


Svelte

<!-- VRViewer.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';

  export let sceneId: string;

  let container: HTMLDivElement;
  let viewerPromise: Promise<any> | null = null;

  onMount(() => {
    viewerPromise = XRGallery.create({
      element: container,
      sceneId
    });
  });

  onDestroy(() => {
    viewerPromise?.then(v => v.dispose());
  });
</script>

<div bind:this={container} style="width: 100%; height: 100vh;"></div>

SvelteKit

For SvelteKit, ensure the component only runs client-side:

<!-- +page.svelte -->
<script>
  import { browser } from '$app/environment';
  import VRViewer from '$lib/VRViewer.svelte';
</script>

{#if browser}
  <VRViewer sceneId="abc123" />
{/if}

Astro

---
// src/pages/tour.astro
---
<html>
<head>
  <title>VR Tour</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    #app { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="app"></div>
  <script is:inline src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
  <script is:inline>
    XRGallery.create({
      element: '#app',
      sceneId: 'abc123'
    });
  </script>
</body>
</html>

Or with a React component using client:only:

---
import VRViewer from '../components/VRViewer';
---

<VRViewer client:only="react" sceneId="abc123" />

Vanilla JavaScript

Script Tag

<div id="app" style="width: 100%; height: 100vh;"></div>

<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
<script>
  XRGallery.create({
    element: '#app',
    sceneId: 'abc123'
  }).then(viewer => {
    console.log('Viewer ready');

    // Clean up on page unload
    window.addEventListener('beforeunload', () => viewer.dispose());
  });
</script>

ES Module (via CDN)

<div id="app" style="width: 100%; height: 100vh;"></div>

<script type="module">
  import 'https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js';

  const viewer = await XRGallery.create({
    element: '#app',
    config: {
      experience: { title: "My Tour" },
      stages: [
        {
          id: "main",
          name: "Main",
          skybox: { type: "image", url: "/panorama.jpg" }
        }
      ]
    }
  });
</script>

Embedding via Iframe

If you can't use the npm package, embed a published tour via iframe:

<iframe
  src="https://xrgallery.online/viewer/YOUR_SCENE_ID"
  width="100%"
  height="600"
  frameborder="0"
  allow="xr-spatial-tracking; fullscreen; autoplay"
  allowfullscreen
></iframe>

This works in any framework without any JavaScript setup.


Common Patterns

Loading State

Show a loading indicator while the viewer initializes:

const container = document.getElementById('app');
const loader = document.createElement('div');
loader.textContent = 'Loading tour...';
loader.className = 'loader';
container.appendChild(loader);

const viewer = await XRGallery.create({
  element: container,
  sceneId: 'abc123'
});
// Viewer replaces the container content with a canvas

Conditional Rendering

// Only create viewer if container exists
const el = document.getElementById('vr-viewer');
if (el) {
  XRGallery.create({ element: el, sceneId: 'abc123' });
}

Multiple Viewers

Each create() call produces an independent viewer. Make sure containers have explicit dimensions:

const viewer1 = await XRGallery.create({
  element: '#viewer-left',
  sceneId: 'scene-a'
});

const viewer2 = await XRGallery.create({
  element: '#viewer-right',
  sceneId: 'scene-b'
});

Performance

Running multiple 3D viewers simultaneously is GPU-intensive. Test performance on target devices.