Framework Integration Guides¶
Examples for using @xrgallery/viewer with popular frameworks and meta-frameworks.
All examples assume you've installed the package:
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:
Script loading
Add the viewer bundle to your angular.json scripts array or load it via a <script> tag in index.html:
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.