Skip to content

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.

ts
// Configure once at startup
Fold.init({ });

// Update live options at any point after init
Fold.set({ });

init() and set() will not re-scan the DOM for content changes your own JS makes after the scene is built — for that, see Fold.refresh() in the Scene API.


Quick reference ​

KeyDefaultset()Description
key''—Key for production domains
foldpagefalse✓Render entire <body> without a wrapper class
cdnUrl'https://cdn.fold.page'—CDN origin for font assets
coreUrlbundled by your app / script directory—Where the loader fetches fold.core.js / fold.three.js from
cdnPath'/'—CDN path prefix
cameraControlstrue✓RPG-style mouse-look + WASD fly controls
cameraButtonfalse✓MOVE CAMERA button in the corner where WebXR isn't available
cameraHudtrue✓Camera HUD data panel and its OPEN / CLOSE HUD toggle
xrButtontrue✓Default ENTER XR button on WebXR devices
cameraOffsetall zeros✓Shift / rotate the camera from its default position
loadingIndicator—✓Loading indicators: DOM ring while Fold loads, optional 3D ring while the scene builds
backsidetrue✓Render back faces of geometry
textSelectiontrue✓Enable 3D text selection
autoOriginXrtrue✓Auto-recentre scene in front of headset in XR
viewDim0.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.

ts
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.

ts
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.

foldpage is live-settable via Fold.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.

ts
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.ttf

setCdn() is a convenience alternative to passing cdnUrl / cdnPath directly:

ts
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 '/'.

ts
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.

ts
Fold.init({ coreUrl: 'https://static.example.com/fold/' });

cameraControls ​

RPG-style camera — Pointer-lock mouse-look combined with WASD fly controls. Enabled by default.

ts
Fold.init({ cameraControls: false }); // disable at startup
Fold.set({ cameraControls: false });  // disable at runtime
Fold.set({ cameraControls: true });   // re-enable

Activating 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 / InputAction
Mouse moveLook around (yaw + pitch)
W / SFly forward / backward
A / DStrafe left / right
R / FFly 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: false they 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.
InputAction
Tilt the deviceRotate the camera in place (yaw, pitch and roll), relative to its orientation when the mode started
One-finger dragMove the camera along its own X/Y axes. Content follows the finger.
Pinch open / closedMove the camera forward / back along its own Z axis
Two-finger dragMove 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:

ButtonWhenAction
FREEZE CAMERACamera mode activeEnds camera mode and leaves the camera where it is
MOVE CAMERACamera frozen away from its baselineTurns camera mode back on (pointer lock on desktop, touch mode on phones)
RESET CAMERACamera frozen away from its baselineReturns 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.

ts
Fold.init({ cameraButton: true });
Fold.set({ cameraButton: false }); // hide it again at runtime

While 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.

ts
Fold.init({ cameraHud: false });
Fold.set({ cameraHud: true }); // bring it back at runtime

xrButton ​

Hide the default ENTER XR button. When false, the bottom-right corner never shows ENTER XR, even on devices with WebXR. Defaults to true.

ts
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.

ts
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:

ts
// 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 ​

FieldUnitDefaultDescription
xCSS px0Shift right (+) or left (−)
yCSS px0Shift up (+) or down (−)
zCSS px0Shift toward content (+) or away (−)
rotXdegrees0Pitch: positive = nose down
rotYdegrees0Yaw: positive = turning right
rotZdegrees0Roll: positive = clockwise

Rotation is applied yaw → pitch → roll.

HTML form: data-foldpage-camera-offset-x/y/z and data-foldpage-camera-rotation-x/y/z on the Fold root element — see Camera Offset. Attributes are read once at init; use Fold.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 builds
  • false: neither — the DOM ring is removed as soon as init() sees this
ts
Fold.init({ loadingIndicator: true });  // DOM ring → 3D ring
Fold.init({ loadingIndicator: false }); // no indicators at all

backside ​

Back-face rendering — Controls whether the back faces of 3D geometry are rendered (THREE.DoubleSide vs THREE.FrontSide). Defaults to true.

ts
Fold.init({ backside: false }); // single-sided geometry
Fold.set({ backside: false });  // update at runtime

Disabling 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.

ts
Fold.init({ textSelection: false }); // disable
Fold.set({ textSelection: false });  // update at runtime

Disable 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.

ts
Fold.init({ autoOriginXr: false });
Fold.set({ autoOriginXr: false }); // update at runtime

See 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.

ts
Fold.init({ viewDim: 0.5 }); // non-active views at 50% opacity
Fold.set({ viewDim: 0.2 });  // update at runtime

Opacity 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.

modeBehavior
'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.

ts
Fold.init({ viewDimCamera: { full: 800, fade: 400 } });
Fold.set({ viewDimCamera: { mode: 'full' } }); // update at runtime

onReady ​

Scene-ready callback — Called once the 3D scene is ready. Add custom Three.js objects and call Scene API methods here.

ts
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.

ts
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.

ts
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', '');
    }
  },
});
PropertyTypeDescription
isActivebooleantrue while a WebXR session is running
mode'immersive-ar' | 'immersive-vr' | nullActive 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.

ts
Fold.init({
  onViewLinkChange: ({ el, targetEl, active }) => {
    if (!active) return;
    console.log('view-link activated:', el, '→ framing', targetEl);
  },
});
PropertyTypeDescription
elHTMLElement | nullThe clicked data-foldpage-view-link element, or null on dismiss
targetElHTMLElement | nullThe element actually framed by the camera — differs from el for #id-anchor links, equals el otherwise
activebooleantrue 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.

ts
Fold.init({ particles: { count: 500, friction: 0.9 } });
Fold.set({ particles: { friction: 0.85 } }); // update at runtime

hdr ​

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:

ts
Fold.init({ hdr: '/environments/night-sky.hdr' });

Pass an object for full control:

ts
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:

ts
Fold.set({ hdr: { background: 'xr' } });    // switch background mode only
Fold.set({ hdr: { intensity: 0.5 } });       // adjust intensity only

background values ​

ValueBackground 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 ​

MaterialIBL response
GLB modelsAlways respond — MeshStandardMaterial / MeshPhysicalMaterial
Elements with data-foldpage-litRespond to IBL and scene lights (all presets except glow)
Unlit text and backgroundsNo 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().

Fold.page — 3D UI for the web.