Spline Self-Hosted Files in Webflow

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.
Step 1: Export Your Scene from Spline
- Open your Spline scene.
- Click Export in the top-right menu.
- Choose Vanilla JS as the export type (not React or Next.js).
- Click Download Self Hosted ZIP.
- Unzip the downloaded archive.
You should see the following files:
scene.splinecode
process.js
process.wasm
runtime.js
- (optional)
physics.js
andopentype.js
Step 2: Upload the Files to BunnyCDN
- Log in to your Bunny.net dashboard.
- Navigate to your Pull Zone or create a new one.
- Upload all the files from the ZIP into a folder like:
https://your-cdn.b-cdn.net/spline-scene/
- Ensure these files are publicly accessible:
runtime.js
scene.splinecode
process.wasm
process.js
- (optional)
opentype.js
- (optional)
physics.js
- In Bunny, go to Caching & Delivery > Request Settings > Add CORS Headers:
- File extensions:
js, wasm, splinecode
- Header:
Access-Control-Allow-Origin
, value:*
- File extensions:
- Clear the cache (optional but recommended).
Step 3: Embed the Scene in Webflow
- In Webflow, drag an Embed element into the desired section of your layout.
- Paste the following code, replacing all URLs with your actual CDN paths:
<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>
⚠️ Common Pitfalls
- Double-check that you selected Vanilla JS during export.
- Ensure all runtime files come from the same export package.
- Confirm that CORS headers and MIME types are correctly set in Bunny.
- Webflow preview mode may not load external scripts—publish your site to test properly.
✅ You're Done!
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.
Advanced Setup for Multiple Spline Scenes on the Same Page
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.
✅ Key Benefits of This Setup
- Runtime is loaded only once, no matter how many scenes you have
- Each scene loads lazily as it scrolls into view
- No duplicate downloads of runtime or assets
- Modular & readable: All scenes are configured clearly using variables
- Separation of concerns: Code lives in your global footer; canvases live where your scenes appear
🔧 How It Works
- Add the script below to your site-wide footer embed in Webflow.
- On the page, insert a <canvas> element with a unique ID for each scene.
- The script detects when a canvas enters the viewport and loads the associated Spline file.
- All .splinecode files are also prefetched during idle time to speed up future loads.
<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>
Add Canvas Elements in Webflow
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>
Final Notes
This is the best solution if you’re using multiple Spline scenes across a page and want:
- Better scroll performance
- Cleaner markup
- Lower resource usage
- Full control over what loads and when