Your first experience
Introduction
Build a collection and detail route using the current public components. The minimal demo in this repository contains the complete example.
Use an existing Svelte 5 / SvelteKit 2 application with Node.js 24 tooling:
npm install @spatial-elements/sveltekit@beta
Three.js is a peer dependency; modern npm installs it automatically. If you import it
yourself, also declare three@^0.186.1 (and @types/three@^0.186.0 for
TypeScript).
1. Supply your assets and element data
Put a GLB model, an HDR environment and an optional poster in your application's static/assets/demo/ directory. The example uses cube-high.glb, studio.hdr and cube.svg. These files are generated by npm run assets:demo in this repository;
npm packages do not bundle example assets. Replace the URLs with your own files in a real application.
The HDR supplies model lighting as well as the detail background. Set category lighting explicitly
with ContentPage.hdr; the detail view uses its own element.hdr.
Create src/lib/starter.ts:
import { createSpatialTheme, type SpatialElementData } from '@spatial-elements/sveltekit';
export const starterTheme = createSpatialTheme({ id: 'starter', name: 'My collection' });
export const starterElements: SpatialElementData[] = [
{
id: 'cube',
title: 'Cube',
geometry: { low: '/assets/demo/cube-high.glb', high: '/assets/demo/cube-high.glb' },
hdr: '/assets/demo/studio.hdr',
fallbackImage: '/assets/demo/cube.svg'
}
];
Only id, title, geometry and hdr are
required on an element. Both geometry.low and geometry.high must
be supplied. Low is the lightweight version for categories/transitions; High is the detailed
version. For a small model, use the same URL twice: fetching and decoding are shared, not
duplicated. Both exports must use the same origin, scale and orientation. Posters are
recommended for loading, server rendering and devices without WebGPU. Optional fields
include features, action, breadcrumbs, background, model and camera; see the API reference.
The theme supplies shared appearance and a namespace. createSpatialTheme fills
in defaults; customize only what you need, for example colors: { accent: '#a33e36' }.
2. Add one persistent layout
Create src/routes/starter/+layout.svelte:
<script lang="ts">
import type { Snippet } from 'svelte';
import { BrandStageShell } from '@spatial-elements/sveltekit';
import { starterTheme } from '$lib/starter';
let { children }: { children: Snippet } = $props();
</script>
<BrandStageShell theme={starterTheme}>
{@render children()}
</BrandStageShell>
Keep the shell mounted while navigating between category and detail routes. It owns the
renderer and shared resources. Pages register their own data with this shell; the layout
never reads page.data, selects a model, or constructs a catalog.
3. Build the category page
Create src/routes/starter/categories/list/+page.ts:
import { getSpatialListItems } from '@spatial-elements/sveltekit';
import { starterElements } from '$lib/starter';
export function load() {
return { items: getSpatialListItems(starterElements, '/starter/elements') };
}
getSpatialListItems derives titles, links, summary content, posters and 3D assets
from the same element documents used by the detail pages. There is no second catalog configuration
to maintain.
Create src/routes/starter/categories/list/+page.svelte:
<script lang="ts">
import { HeadingSection } from '@page-sections/svelte';
import { ContentPage, SpatialCardGridSection } from '@spatial-elements/sveltekit';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<ContentPage hdr="/assets/demo/studio.hdr" page={{ title: 'My collection' }}>
<HeadingSection section={{ id: 'collection-title', title: 'My collection', headingLevel: 1 }} />
<SpatialCardGridSection section={{ id: 'elements', title: 'Elements', headline: 'Elements' }} list={data.items} />
</ContentPage>
The required hdr prop belongs to the category page. It can point to the same environment
as an element or a different one; changing the list order never changes the page environment.
A ContentPage can contain multiple SpatialCardGridSection, SpatialCarouselSection and ordinary Section components. Section IDs must be unique within the page. To
repeat the same element inside one section, supply a distinct itemKey on each occurrence.
4. Build the detail page
Create src/routes/starter/elements/[id]/+page.ts:
import { error } from '@sveltejs/kit';
import { findSpatialElement } from '@spatial-elements/sveltekit';
import { starterElements } from '$lib/starter';
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => {
const element = findSpatialElement(starterElements, params.id);
if (!element) error(404, 'Element not found');
return { element };
};
Create src/routes/starter/elements/[id]/+page.svelte:
<script lang="ts">
import { SpatialElementPage, Section } from '@spatial-elements/sveltekit';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<SpatialElementPage spatialElement={data.element}>
<Section section={{ id: 'details', title: 'About this element', headline: 'About this element', background: { color: 'standard' } }}>
<p>Rotate the model, zoom in, or return to the collection.</p>
<a href="/starter/categories/list">Back to the collection</a>
</Section>
</SpatialElementPage>
SpatialElementPage reads the theme from its enclosing shell and supplies its
own model data to the renderer. Its Section children become anchored content and
generated detail navigation. Content without an action or feature list works without placeholder
buttons. The default media rail contains one interactive minimap.
5. Run and extend
Start your application's dev server and open /starter/categories/list. Follow
the cube link, reload the detail page, and use browser Back. A working version of this
example is included in the public demo at the same URL and covered by browser tests,
including a JavaScript-disabled visit.
Current transition recipes recognize /:namespace/categories/{list,carousel,mixed} and /:namespace/elements/:id (also products/:id). Use the theme ID
as the namespace. Arbitrary route patterns are not yet configurable. Resolve data in route
load functions before rendering; the returned property names (items and element here) are your choice. An API or CMS can replace the local array.
WebGPU enhancement requires a capable browser/device and a secure context (HTTPS or
localhost). Semantic HTML, links and supplied posters remain available without it. If models
use Draco compression, serve Three.js's examples/jsm/libs/draco/gltf/ decoder
files at /assets/draco/gltf/, or pass a different directory, with trailing
slash, as dracoDecoderPath on the shell. Adjust asset, decoder and link URLs for
your deployment base path.
The default 3D render budget is a maximum pixel ratio of 1 and 2,073,600 pixels per canvas.
HTML stays at native resolution. Only change renderSettings after measuring your
target devices. For larger assets, use genuinely simplified Low geometry and a detailed High export
in a shared coordinate frame. URL-only geometry is fitted from Low; generated bounds/transforms
are optional advanced metadata. Merely giving the same large model two roles does not reduce its
rendering cost.
Continue with the API reference, composition guide and asset contract.