Skip to content

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

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

const viewer = await createViewerFromSceneId(container, sceneId, {
  lazyLoad: true
});

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