Basic Usage
Learn the fundamentals of embedding a StorySplat viewer in your web application.
Installation
# npm
npm install storysplat-viewer
# yarn
yarn add storysplat-viewer
# pnpm
pnpm add storysplat-viewer
CDN Usage
For quick prototyping, use the bundled build (includes PlayCanvas - only one script needed!):
<!-- Recommended: Bundled build -->
<script src="https://cdn.jsdelivr.net/npm/storysplat-viewer@2/dist/storysplat-viewer.bundled.umd.js"></script>
<script>
const { createViewerFromSceneId } = StorySplatViewer;
</script>
If you need a specific PlayCanvas version, load them separately:
<script src="https://cdn.jsdelivr.net/npm/playcanvas@2.14.3/build/playcanvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/storysplat-viewer@2/dist/storysplat-viewer.umd.js"></script>
Quick Start
Minimal Example
<!DOCTYPE html>
<html>
<head>
<style>
#viewer { width: 100%; height: 600px; }
</style>
</head>
<body>
<div id="viewer"></div>
<script type="module">
import { createViewerFromSceneId } from 'storysplat-viewer';
const viewer = await createViewerFromSceneId(
document.getElementById('viewer'),
'YOUR_SCENE_ID'
);
</script>
</body>
</html>
Loading Scenes
Option 1: From Scene ID (Recommended)
Use this when you want your scene to always show the latest version from the StorySplat editor.
import { createViewerFromSceneId } from 'storysplat-viewer';
const viewer = await createViewerFromSceneId(
document.getElementById('viewer'),
'YOUR_SCENE_ID',
{
autoPlay: false,
showUI: true
}
);
How to get your Scene ID: 1. Open your scene in the StorySplat editor at storysplat.com 2. Click "Upload" or "Update" to save your scene 3. In the "Developer Export" section, copy the Scene ID
Option 2: From JSON File (Version Controlled)
Use this when you want to version control your scene configuration.
import { createViewer } from 'storysplat-viewer';
import sceneConfig from './scenes/my-scene.json';
const viewer = createViewer(
document.getElementById('viewer'),
sceneConfig,
{ autoPlay: true }
);
Option 3: From URL
import { createViewerFromUrl } from 'storysplat-viewer';
const viewer = await createViewerFromUrl(
document.getElementById('viewer'),
'https://your-cdn.com/scenes/my-scene.json'
);
Comparison Table
| Method | Best For | Updates | Offline |
|---|---|---|---|
| Scene ID | Live content, CMS | Automatic | No |
| JSON File | Version control, CI/CD | Manual | Yes |
| URL | Custom backends, S3 | Depends | No |
Framework Examples
React
import { useEffect, useRef } from 'react';
import { createViewerFromSceneId } from 'storysplat-viewer';
function StorySplatViewer({ sceneId, onReady }) {
const containerRef = useRef(null);
const viewerRef = useRef(null);
useEffect(() => {
let mounted = true;
async function init() {
if (!containerRef.current) return;
const viewer = await createViewerFromSceneId(
containerRef.current,
sceneId
);
if (mounted) {
viewerRef.current = viewer;
viewer.on('ready', () => onReady?.());
} else {
viewer.destroy();
}
}
init();
return () => {
mounted = false;
viewerRef.current?.destroy();
};
}, [sceneId]);
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}
Vue 3
<template>
<div ref="containerRef" class="viewer-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { createViewerFromSceneId } from 'storysplat-viewer';
const props = defineProps(['sceneId']);
const containerRef = ref(null);
let viewer = null;
onMounted(async () => {
if (!containerRef.value) return;
viewer = await createViewerFromSceneId(containerRef.value, props.sceneId);
});
onUnmounted(() => {
viewer?.destroy();
});
</script>
<style scoped>
.viewer-container { width: 100%; height: 600px; }
</style>
Next.js (App Router)
'use client';
import { useEffect, useRef } from 'react';
export default function StorySplatViewer({ sceneId }) {
const containerRef = useRef(null);
const viewerRef = useRef(null);
useEffect(() => {
let mounted = true;
async function init() {
if (!containerRef.current) return;
// Dynamic import to avoid SSR issues
const { createViewerFromSceneId } = await import('storysplat-viewer');
const viewer = await createViewerFromSceneId(
containerRef.current,
sceneId
);
if (mounted) {
viewerRef.current = viewer;
} else {
viewer.destroy();
}
}
init();
return () => {
mounted = false;
viewerRef.current?.destroy();
};
}, [sceneId]);
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}
Vanilla JavaScript
<div id="viewer"></div>
<script type="module">
import { createViewerFromSceneId } from 'storysplat-viewer';
const viewer = await createViewerFromSceneId(
document.getElementById('viewer'),
'YOUR_SCENE_ID'
);
viewer.on('ready', () => {
console.log('Viewer ready!');
});
</script>
Configuration Options
interface ViewerOptions {
autoPlay?: boolean; // Auto-start tour playback (default: false)
showUI?: boolean; // Show navigation UI (default: true)
backgroundColor?: string; // Background color
revealEffect?: 'fast' | 'medium' | 'slow' | 'none';
lazyLoad?: boolean; // Show thumbnail + start button first
lazyLoadThumbnail?: string; // Custom thumbnail URL
lazyLoadButtonText?: string; // Custom button text
}
Examples
// Minimal
const viewer = await createViewerFromSceneId(container, sceneId);
// With autoplay
const viewer = await createViewerFromSceneId(container, sceneId, {
autoPlay: true
});
// With lazy loading
const viewer = await createViewerFromSceneId(container, sceneId, {
lazyLoad: true,
lazyLoadThumbnail: '/preview.jpg',
lazyLoadButtonText: 'Start Tour'
});
// Custom reveal effect
const viewer = await createViewerFromSceneId(container, sceneId, {
revealEffect: 'slow'
});
Styling & Layout
The container must have explicit dimensions:
/* Good */
#viewer {
width: 100%;
height: 600px;
}
/* Bad - no height */
#viewer {
width: 100%;
/* height defaults to 0! */
}
Responsive Layout
#viewer {
width: 100%;
height: 60vh;
min-height: 400px;
max-height: 800px;
}
@media (max-width: 768px) {
#viewer {
height: 50vh;
min-height: 300px;
}
}
Performance Tips
Lazy Loading
Intersection Observer
Only load when visible:
const observer = new IntersectionObserver(async (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const viewer = await createViewerFromSceneId(
entry.target,
entry.target.dataset.sceneId
);
observer.unobserve(entry.target);
}
}
}, { threshold: 0.1 });
document.querySelectorAll('[data-scene-id]').forEach(el => {
observer.observe(el);
});
Always Cleanup
// React
useEffect(() => {
return () => viewer?.destroy();
}, []);
// Vanilla
window.addEventListener('beforeunload', () => {
viewer.destroy();
});
Next Steps
- 4DGS Playback - Volumetric video
- API Reference - Complete API
- Events - Event handling