Configuration API
Fold.init() and Fold.set() share the same configuration options. init() accepts all of them at startup. set() accepts a subset — options that can be applied to a live session without reinitialising.
// Configure once at startup
Fold.init({ });
// Update live options at any point after init
Fold.set({ });
init()andset()will not re-scan the DOM for content changes your own JS makes after the scene is built — for that, seeFold.refresh()in the Scene API.
Quick reference
| Key | Default | set() | Description |
|---|---|---|---|
key | '' | — | Key for production domains |
foldpage | false | ✓ | Render entire <body> without a wrapper class |
cdnUrl | 'https://cdn.fold.page' | — | CDN origin for font assets |
coreUrl | bundled by your app / script directory | — | Where the loader fetches fold.core.js / fold.three.js from |
cdnPath | '/' | — | CDN path prefix |
cameraControls | true | ✓ | RPG-style mouse-look + WASD fly controls |
cameraButton | false | ✓ | MOVE CAMERA button in the corner where WebXR isn't available |
cameraHud | true | ✓ | Camera HUD data panel and its OPEN / CLOSE HUD toggle |
xrButton | true | ✓ | Default ENTER XR button on WebXR devices |
cameraOffset | all zeros | ✓ | Shift / rotate the camera from its default position |
loadingIndicator | — | ✓ | Loading indicators: DOM ring while Fold loads, optional 3D ring while the scene builds |
backside | true | ✓ | Render back faces of geometry |
textSelection | true | ✓ | Enable 3D text selection |
autoOriginXr | true | ✓ | Auto-recentre scene in front of headset in XR |
viewDim | 0.8 | ✓ | Non-active view opacity |
viewDimCamera | { mode: 'distance', full: 1000, fade: 600 } | ✓ | View dimming while the free camera roams |
onReady | — | — | Called once the scene is fully initialised |
onError | — | — | Called if an error occurs during initialisation |
onXrChange | — | ✓ | Called whenever a WebXR session starts or ends |
onViewLinkChange | — | ✓ | Called whenever a data-foldpage-view-link element is activated or dismissed |
particles | — | ✓ | GPU particle system config |
hdr | — | ✓ | HDRI environment: string path or { src, background, intensity, blur } |
Options without ✓ in the set() column are init-only — they configure the bootstrap and cannot be changed after Fold.init().
Options
key
key — Required for production use. A key is issued for one domain and covers that domain and its subdomains (e.g. example.com, www.example.com, app.example.com), over http or https. Local development works without a key on localhost, IP addresses, [::1], and *.localhost / *.test / *.local hostnames.
Fold.init({ key: 'YOUR_KEY' });See fold.page for information about getting a key.
foldpage
Render entire page — When true, Fold renders the entire <body> in 3D without requiring a wrapper element. When false (default), only content inside .foldpage is rendered.
Fold.init({ foldpage: true });This is the simplest way to get started: drop Fold.page onto any existing page and render it all at once without modifying your HTML.
Use the Target Wrapper approach instead when you want to render only a specific region in 3D.
foldpageis live-settable viaFold.set(), but changing it after the initial DOM render has no effect on elements already in the scene.
cdnUrl
Font origin — Fold loads its font files at runtime from this URL; they aren't part of the JS bundle. Defaults to 'https://cdn.fold.page'. Change it only if you serve the font files yourself.
Fold.init({
cdnUrl: 'https://your-cdn.example.com',
cdnPath: '/fold-assets/',
});Fonts are requested at paths like:
https://your-cdn.example.com/fold-assets/fonts/Lato/Lato-Regular.ttfsetCdn() is a convenience alternative to passing cdnUrl / cdnPath directly:
Fold.setCdn('https://your-cdn.example.com', '/fold-assets/');
Fold.init();The fonts ship in the npm package at node_modules/fold-page/dist-lib/fonts/. To self-host them, see Install Guide — Runtime files.
cdnPath
Font path prefix — Path added after cdnUrl in font URLs. Defaults to '/'.
Fold.init({
cdnUrl: 'https://your-cdn.example.com',
cdnPath: '/fold-assets/',
});coreUrl
Core files location — Where Fold loads fold.core.js and fold.three.js from. Usually not needed:
- npm (
import … from 'fold-page'): your bundler adds these files to your app's build from the installed package. - Script tag (
<script src="…/fold.standalone.js">): they load from the same folder as the script.
Set coreUrl only to load them from somewhere else. fold-page/esm ignores it, because it is a single file.
Fold.init({ coreUrl: 'https://static.example.com/fold/' });cameraControls
RPG-style camera — Pointer-lock mouse-look combined with WASD fly controls. Enabled by default.
Fold.init({ cameraControls: false }); // disable at startup
Fold.set({ cameraControls: false }); // disable at runtime
Fold.set({ cameraControls: true }); // re-enableActivating camera mode
Camera mode (pointer lock + movement) activates in two ways:
- Hold right mouse button for 0.5 seconds over the canvas — activates mid-hold, no need to release
- Hold W, A, S, or D for 0.5 seconds — the first held WASD key starts the timer
- Shift+P — activates immediately, no hold required
Once active, the cursor is hidden and mouse movement steers the camera. WASD and mouse look are coupled — both are only live while the mode is active.
Deactivating camera mode
- Right-click while in camera mode — exits immediately, no context menu
- Left-click — exits immediately
- Shift+P — exits immediately (toggles off)
- Escape — exits immediately (browser default for pointer lock)
- 3 seconds of no input — exits automatically
Right-click (outside camera mode)
- Single right-click — does nothing
- Double right-click (two clicks under 0.5 seconds apart) — opens the native browser context menu
Controls
| Key / Input | Action |
|---|---|
| Mouse move | Look around (yaw + pitch) |
| W / S | Fly forward / backward |
| A / D | Strafe left / right |
| R / F | Fly up / down |
Touch devices
On touch (coarse-pointer) devices the same flag enables a touch camera mode. While it's active, page scrolling and pinch-zoom are paused and taps don't activate page content.
- Activate: use either gesture below. The mode activates mid-hold, and Android devices give a short haptic pulse.
- Long press (the main way): hold one finger still for 2 s.
- Double-tap and hold: tap once, then press again within 0.4 s and hold for 0.4 s.
- On text, these gestures select text instead: a long press starts a selection after 0.5 s, and a double-tap and hold after 0.4 s. Anywhere else, they start the camera. With
textSelection: falsethey start the camera everywhere. - Deactivate: use either gesture again, tap FREEZE CAMERA, or stop moving. The mode exits after 2.5 seconds with no finger on the screen and no device tilt. It also exits when the page loses focus.
| Input | Action |
|---|---|
| Tilt the device | Rotate the camera in place (yaw, pitch and roll), relative to its orientation when the mode started |
| One-finger drag | Move the camera along its own X/Y axes. Content follows the finger. |
| Pinch open / closed | Move the camera forward / back along its own Z axis |
| Two-finger drag | Move the camera along X/Y (works while pinching too) |
Camera HUD and corner buttons
While camera mode is active, the bottom-right corner shows camera controls in place of the Enter XR button:
| Button | When | Action |
|---|---|---|
| FREEZE CAMERA | Camera mode active | Ends camera mode and leaves the camera where it is |
| MOVE CAMERA | Camera frozen away from its baseline | Turns camera mode back on (pointer lock on desktop, touch mode on phones) |
| RESET CAMERA | Camera frozen away from its baseline | Returns the camera to its baseline pose |
On desktop the cursor is hidden while camera mode is active, so press Esc or click to freeze, then click RESET CAMERA.
An OPEN HUD / CLOSE HUD toggle above the corner button shows a live readout of the camera position (CSS px) and rotation (degrees). The readout is closed by default. Its values use cameraOffset units, so a reading can be pasted straight into Fold.set({ cameraOffset }) to make that view the default. Turn the readout off with cameraHud: false.
All movement is relative to the camera's current rotation. On iOS, device tilt needs motion permission, which is requested on the first touch after the mode starts; if it's denied, drag and pinch still work.
When a WebXR session ends, the camera returns to its baseline pose. During a session, EXIT XR sits in the screen corner on phones and at the bottom-right of the page on headsets.
cameraButton
Always one button in the corner. When true, the bottom-right corner always holds exactly one button from page load:
- ENTER XR on devices with WebXR
- MOVE CAMERA everywhere else
MOVE CAMERA turns on the camera controls: pointer lock on desktop, touch camera mode on phones. It requires cameraControls, which is on by default.
When false or not set, the corner shows ENTER XR on WebXR devices only.
Fold.init({ cameraButton: true });
Fold.set({ cameraButton: false }); // hide it again at runtimeWhile camera mode is active or the camera is offset, the corner shows FREEZE CAMERA / RESET CAMERA as usual. After a reset it returns to MOVE CAMERA.
cameraHud
Turn off the camera HUD panel. When false, the camera HUD's data panel is never shown: position and rotation readouts, radar and dials. The OPEN HUD / CLOSE HUD toggle is hidden as well. The camera buttons (MOVE / FREEZE / RESET CAMERA) and the first-session MOVE / FREEZE instructions still appear. Defaults to true.
Fold.init({ cameraHud: false });
Fold.set({ cameraHud: true }); // bring it back at runtimexrButton
Hide the default ENTER XR button. When false, the bottom-right corner never shows ENTER XR, even on devices with WebXR. Defaults to true.
Fold.init({ xrButton: false });
Fold.set({ xrButton: true }); // show it again at runtime- EXIT XR still appears during a session, so a session started some other way can always be ended.
- With
cameraButton, the corner shows MOVE CAMERA instead, on every device.
cameraOffset
Camera baseline position — Shifts and rotates the camera from its default screen-mode position. All other camera behaviors (scroll-driven view navigation, WASD controls) operate on top of this offset as the new baseline pose. Screen mode only — in WebXR the headset pose drives the camera and this setting has no effect.
Framing is your responsibility. Fold does not adjust the scene to compensate for the camera offset. Large values will move the camera away from the default framing of the page content.
Fold.init({
cameraOffset: {
x: -150, // shift left 150 CSS px
y: -120, // shift down 120 CSS px
z: 0, // no depth change (positive = toward content)
rotX: 9, // pitch down 9°
rotY: -14, // yaw left 14°
rotZ: 0, // no roll
},
});Runtime updates via Fold.set() — partial updates merge into the current offset:
// Restore default camera position
Fold.set({ cameraOffset: { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0 } });
// Partial update — only the specified axes change
Fold.set({ cameraOffset: { rotY: -10 } });Fields
| Field | Unit | Default | Description |
|---|---|---|---|
x | CSS px | 0 | Shift right (+) or left (−) |
y | CSS px | 0 | Shift up (+) or down (−) |
z | CSS px | 0 | Shift toward content (+) or away (−) |
rotX | degrees | 0 | Pitch: positive = nose down |
rotY | degrees | 0 | Yaw: positive = turning right |
rotZ | degrees | 0 | Roll: positive = clockwise |
Rotation is applied yaw → pitch → roll.
HTML form:
data-foldpage-camera-offset-x/y/zanddata-foldpage-camera-rotation-x/y/zon the Fold root element — see Camera Offset. Attributes are read once at init; useFold.set({ cameraOffset })for runtime updates.
loadingIndicator
Loading indicators — A lightweight DOM ring is shown while Fold's JavaScript downloads and boots, from the moment Fold's own code first runs: when fold.standalone.js executes (script tag / npm default import), or when the bundle containing fold-page/esm evaluates in a bundled app — before the app has bootstrapped or called Fold.init(). A page that includes Fold but never calls init() sees it disappear again after a few seconds. It hands off to the in-scene 3D indicator, or fades out on fold-ready when the 3D one is off.
- not set (default): DOM ring only — the 3D indicator stays off
true: DOM ring, then the 3D indicator while the scene buildsfalse: neither — the DOM ring is removed as soon asinit()sees this
Fold.init({ loadingIndicator: true }); // DOM ring → 3D ring
Fold.init({ loadingIndicator: false }); // no indicators at allbackside
Back-face rendering — Controls whether the back faces of 3D geometry are rendered (THREE.DoubleSide vs THREE.FrontSide). Defaults to true.
Fold.init({ backside: false }); // single-sided geometry
Fold.set({ backside: false }); // update at runtimeDisabling back-face rendering can improve performance and avoids z-fighting artefacts when geometry is only ever viewed from the front.
Per-element override: Add
data-foldpage-backface="false"to individual<img>elements to disable back faces for that image only, regardless of this global setting — see Data Attribute API → backface.
textSelection
3D text selection — Controls whether text in the 3D scene is selectable (highlight, copy). Defaults to true.
Fold.init({ textSelection: false }); // disable
Fold.set({ textSelection: false }); // update at runtimeDisable when text selection interferes with custom pointer interactions.
On touch devices a selection starts with a long press on text (0.5 s) or a double-tap and hold (tap, then press and hold 0.4 s). Anywhere else, those gestures start the touch camera controls (see cameraControls). On text, selection always takes priority.
autoOriginXr
XR auto-recentre — When true (the default), Fold automatically repositions the 3D scene in front of the headset when the user moves or rotates beyond a threshold in WebXR. Set to false to keep the scene anchored at a fixed world position.
Fold.init({ autoOriginXr: false });
Fold.set({ autoOriginXr: false }); // update at runtimeSee XR Features — Auto-Origin for details.
viewDim
Non-active view opacity — Controls the opacity of scroll-driven Views that are not currently in focus. Defaults to 0.8. Set to 1 to disable dimming entirely.
Fold.init({ viewDim: 0.5 }); // non-active views at 50% opacity
Fold.set({ viewDim: 0.2 }); // update at runtimeOpacity transitions smoothly as the camera travels between views. To override the dim value for a specific group of views, place data-foldpage-view-dim on an ancestor element.
viewDimCamera
View dimming with the free camera — Applies while cameraControls has moved the camera away from its reset pose (camera mode active, or frozen and not yet reset). Views fade into this behavior when the camera leaves the reset pose and fade back when it is reset.
mode | Behavior |
|---|---|
'distance' (default) | Each view is at full opacity while the camera is within full px of it, fades over the next fade px, and is at its normal viewDim beyond that. The active view (the one you scrolled or linked to) always stays at full opacity. Distance is measured to the nearest point of the view's rectangle. |
'full' | Every view fades to full opacity. |
'off' | Normal scroll dimming is kept. |
full (default 1000) and fade (default 600) apply as given to a view framed at the natural 500px camera distance, and scale with each view's framing distance. A large view framed at 800px uses 1.6× both values, so it is not dimmed from where it is normally seen. Fold.set() merges, so you can change a single field.
Fold.init({ viewDimCamera: { full: 800, fade: 400 } });
Fold.set({ viewDimCamera: { mode: 'full' } }); // update at runtimeonReady
Scene-ready callback — Called once the 3D scene is ready. Add custom Three.js objects and call Scene API methods here.
Fold.init({
onReady: () => {
const THREE = Fold.getThree();
const scrollGroup = Fold.getGroupScroll();
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(Fold.getPixelSize(28), 32, 32),
new THREE.MeshStandardMaterial({ color: 'cyan' }),
);
sphere.position.set(Fold.getPixelSize(100), -Fold.getPixelSize(100), 0.05);
scrollGroup.add(sphere);
},
});Any Scene API call made before onReady fires will return null — the scene and its groups do not exist yet.
onError
Init error callback — Called if an error is thrown during Fold initialisation. Receives the Error object as its only argument.
Fold.init({
onError: (err) => {
console.error('Fold failed to initialise:', err);
},
});onXrChange
XR session callback — Called whenever a WebXR session starts or ends. Fires with isActive: true on session start and isActive: false on session end.
Fold.init({
onXrChange: ({ isActive, mode }) => {
const nav = document.querySelector('.site-nav');
if (nav) nav.style.visibility = isActive ? 'hidden' : '';
if (mode === 'immersive-ar') {
document.querySelector('.ar-prompt')?.removeAttribute('hidden');
} else {
document.querySelector('.ar-prompt')?.setAttribute('hidden', '');
}
},
});| Property | Type | Description |
|---|---|---|
isActive | boolean | true while a WebXR session is running |
mode | 'immersive-ar' | 'immersive-vr' | null | Active session mode, or null when inactive |
onXrChange can be replaced at runtime via Fold.set({ onXrChange: newFn }).
To read the current XR state without a callback, use Fold.getXr().
onViewLinkChange
View-link callback — Called whenever a data-foldpage-view-link element is activated (the camera frames it) or dismissed. Fires once per real state transition — not on every click. A second click on an already-active view-link that also happens to be a native interactive element (<a>, <button>, etc.) passes through to that element's own click behavior instead of dismissing, so it does not re-fire this callback.
Fold.init({
onViewLinkChange: ({ el, targetEl, active }) => {
if (!active) return;
console.log('view-link activated:', el, '→ framing', targetEl);
},
});| Property | Type | Description |
|---|---|---|
el | HTMLElement | null | The clicked data-foldpage-view-link element, or null on dismiss |
targetEl | HTMLElement | null | The element actually framed by the camera — differs from el for #id-anchor links, equals el otherwise |
active | boolean | true on activation, false on dismiss |
onViewLinkChange can be replaced at runtime via Fold.set({ onViewLinkChange: newFn }).
particles
Particle system — GPU particle system configuration. See the Particle System guide for the full reference.
Fold.init({ particles: { count: 500, friction: 0.9 } });
Fold.set({ particles: { friction: 0.85 } }); // update at runtimehdr
HDRI environment — Loads an equirectangular .hdr image and applies it as image-based lighting (IBL). All PBR materials and GLB models are illuminated by the environment map. An optional visible background sky is controlled by the background field.
Pass a string to set the path with all other options at their defaults:
Fold.init({ hdr: '/environments/night-sky.hdr' });Pass an object for full control:
Fold.init({
hdr: {
src: '/sky.hdr',
background: 'screen', // sky visible on screen, transparent in AR
intensity: 0.8, // softer IBL
blur: 0.2, // softly defocused sky
},
});IBL is always active once src is set regardless of background. Leave background at its default (false) for lighting-only mode with no visible sky.
Runtime updates — only the fields you provide are changed:
Fold.set({ hdr: { background: 'xr' } }); // switch background mode only
Fold.set({ hdr: { intensity: 0.5 } }); // adjust intensity onlybackground values
| Value | Background shown |
|---|---|
false (default) | Never — IBL lighting only, HTML background colour fills the canvas |
true or 'both' | Screen and XR |
'screen' | Screen only — in XR the background is transparent (AR passthrough unobstructed) |
'xr' | XR only — on screen the canvas background colour shows instead |
intensity
Scales the strength of IBL on PBR materials. Default 1.0. Does not affect background brightness — only reflected and diffuse light on lit surfaces. Reduce when combining IBL with manual scene lights to prevent overexposure.
blur
Blurs the visible background sky from 0 (sharp, default) to 1 (fully blurred). Has no effect on IBL quality. Ignored when background is false.
IBL and material colors
| Material | IBL response |
|---|---|
| GLB models | Always respond — MeshStandardMaterial / MeshPhysicalMaterial |
Elements with data-foldpage-lit | Respond to IBL and scene lights (all presets except glow) |
| Unlit text and backgrounds | No response — CSS colors are shown exactly |
Stacking with scene lights added via the Scene API: IBL adds to manual lights. Reduce intensity if the scene appears overexposed.
Tip: Use
data-foldpage-lit="vivid"on elements where you want colors close to their CSS values while still receiving a subtle ambient-light influence.
HTML form:
data-foldpage-hdr-src— see Environment (HDRI) for the HTML attribute reference and the CSS class-based visibility approach (foldpage-xr/foldpage-noxr).Fold.init({ hdr })takes precedence if both are present.
For loading .hdr images manually (custom IBL setups), see Fold.getRgbeLoader().

