I’ve created a Webflow showcase to demonstrate this implementation in action. You can explore both the simple and advanced versions — and if you want to try it yourself, you can easily clone and duplicate the project.
Spline recently introduced a powerful feature: the ability to export your 3D scenes as self-hosted packages. This gives you full control over how and where your Spline scenes are hosted—ideal if you're building custom websites and want better performance, CDN-level speed, and independence from third-party hosting.
In this tutorial, I’ll show you how to export a Spline scene using the self-hosted feature, upload the files to BunnyCDN, and embed the scene in your Webflow site using a <canvas> element and the Spline runtime.
While BunnyCDN is used in this guide, any static file host that supports CORS and proper MIME types will work. Alternatives include Cloudflare R2, AWS S3, Netlify, Vercel, or a self-managed server. BunnyCDN is featured here for its excellent performance, affordability, and ease of use.
You should see the following files:
scene.splinecodeprocess.jsprocess.wasmruntime.jsphysics.js and opentype.js https://your-cdn.b-cdn.net/spline-scene/runtime.jsscene.splinecodeprocess.wasmprocess.js(optional) opentype.js(optional) physics.jsjs, wasm, splinecode
<canvas id="spline-container" style="width:100%; height:600px;"></canvas>
<script type="module">
import { Application } from 'https://your-cdn.b-cdn.net/spline-scene/runtime.js';
const canvas = document.getElementById('spline-container');
const app = new Application(canvas, {
wasmPath: 'https://your-cdn.b-cdn.net/spline-scene/'
});
app.load('https://your-cdn.b-cdn.net/spline-scene/scene.splinecode')
.then(() => console.log('Scene loaded'))
.catch((err) => console.error('Load error', err));
</script>If everything is set up correctly, your Spline scene will now render beautifully within your Webflow site. This setup gives you more flexibility, faster load times, and full ownership of your assets.
If something isn’t working, check your browser’s console for errors and verify that all URLs are correct and publicly accessible.
Stay tuned for updates from Spline, as new export formats and features continue to improve the self-hosting workflow.
If your Webflow page features more than one Spline scene, you’ll want a setup that avoids performance issues and repetitive code. This optimized approach ensures all scenes load efficiently, without reloading the same runtime multiple times and keeps everything clean and scalable.
<script>
document.addEventListener("DOMContentLoaded", async function () {
// Path to your self-hosted runtime and .splinecode files
const WASM_PATH = 'https://YOUR_CDN_URL_HERE/spline-files/';
// Load the runtime module once and reuse it
let runtimeModulePromise = import(WASM_PATH + 'runtime.js');
const getRuntime = () => runtimeModulePromise;
// Basic queue system to avoid initializing multiple heavy scenes at once
let queue = Promise.resolve();
const enqueue = (task) => (queue = queue.then(task).catch(() => {}));
// Track which scenes have been loaded
const initialized = new Set();
// Check if a canvas element is hidden
const isHidden = (el) => {
if (!el) return true;
const cs = window.getComputedStyle(el);
return cs.display === 'none' || cs.visibility === 'hidden';
};
// Load a scene into its canvas
async function loadScene({ canvasId, splineUrl }) {
if (initialized.has(canvasId)) return;
const canvas = document.getElementById(canvasId);
if (!canvas || isHidden(canvas)) return;
await enqueue(async () => {
const { Application } = await getRuntime();
const app = new Application(canvas, { wasmPath: WASM_PATH });
await app.load(splineUrl);
initialized.add(canvasId);
console.log(`✅ Loaded Spline scene: ${canvasId}`);
});
}
// Lazy load scenes as they scroll into view
function lazyLoadScene(cfg) {
const canvas = document.getElementById(cfg.canvasId);
if (!canvas || isHidden(canvas) || initialized.has(cfg.canvasId)) return;
const io = new IntersectionObserver(async (entries, obs) => {
if (!entries[0].isIntersecting) return;
obs.disconnect();
await loadScene(cfg);
}, {
rootMargin: '1000px 0px', // Start loading before it's fully in view
threshold: 0.05
});
io.observe(canvas);
}
// ---------- CONFIGURATION START ----------
// Define your scenes here: one entry per canvas
const scenes = [
{
canvasId: 'spline-scene-1',
splineUrl: 'https://YOUR_CDN_URL_HERE/spline-files/scene-1/scene.splinecode'
},
{
canvasId: 'spline-scene-2',
splineUrl: 'https://YOUR_CDN_URL_HERE/spline-files/scene-2/scene.splinecode'
},
{
canvasId: 'spline-scene-3',
splineUrl: 'https://YOUR_CDN_URL_HERE/spline-files/scene-3/scene.splinecode'
}
// Add more scenes as needed...
];
// ---------- CONFIGURATION END ----------
// Prefetch all .splinecode files during idle time (optional but improves UX)
const idle = window.requestIdleCallback || ((fn) => setTimeout(fn, 250));
idle(() => {
scenes.forEach(s => {
try {
fetch(s.splineUrl, { mode: 'no-cors' });
} catch (_) {}
});
});
// Attach lazy observers to each scene
scenes.forEach(lazyLoadScene);
});
</script>For each scene on your page, add a matching canvas element via an Embed element in Webflow:
<canvas id="spline-scene-1" style="width: 100%; height: 100%;"></canvas>
<canvas id="spline-scene-2" style="width: 100%; height: 100%;"></canvas>
<canvas id="spline-scene-3" style="width: 100%; height: 100%;"></canvas>
This is the best solution if you’re using multiple Spline scenes across a page and want: