
Y-axis spatial navigation — panels rise and fall around the origin.
Scroll to climb up through the sky, then descend into the depths.
CSS Position
Fold respects CSS position values and maps each to its 3D equivalent,
preserving the layering and attachment semantics you already know from the browser.
Elements with position: fixed are rendered as non-scrolling
overlays in the 3D scene. They remain locked to the camera view as the
user scrolls — ideal for navigation bars, HUDs, or persistent controls that should
always be visible regardless of scene position.
<!-- Fixed: always visible, doesn't scroll away --> <nav style="position: fixed; top: 0;" class="my-nav">...</nav>
Sticky elements mirror the browser's built-in sticky behavior. While the viewport
scroll is within the parent's bounds, the element is rendered at a fixed
position in 3D space. Once the parent scrolls out of range, it moves with
the scroll group as normal. Fold tracks both states and transitions between them
using a live getBoundingClientRect().top threshold check.
<!-- Sticky: sticks at top until parent scrolls away --> <header style="position: sticky; top: 0;">Section Title</header>
Absolutely positioned elements are placed relative to their nearest positioned
ancestor in the 3D scene graph, matching browser layout semantics.
Fold's findPositionedAncestor() walks the DOM tree upward to locate
the correct parent group and nests the element within it at its CSS-computed offset.
<!-- Absolute: relative to nearest positioned parent --> <div style="position: relative;"> <span style="position: absolute; top: 10px; right: 10px;">Badge</span> </div>
Camera-locked overlay. Does not scroll. Rendered outside the main scroll group.
Two 3D instances: one in scroll group, one fixed — toggled by scroll position threshold.
Nested under its positioned ancestor group. Offset by CSS top/left/right/bottom.
Default flow. Nested by DOM parent–child structure in the scene graph.
view-sky — rotateX(-22deg) translateY(-680px) translateZ(260px).
The camera approached from below, looking slightly upward to read it.
Text & Typography
Fold renders HTML text content as live 3D geometry — not bitmaps or sprites. Each paragraph, heading, link, and inline element is processed by Troika, a WebGL-native text renderer that produces signed-distance-field meshes directly in the Three.js scene.
All standard text containers are handled: <p>,
<h1>–<h6>, <li>,
<blockquote>, <label>, and
<td>. Inline elements — <strong>,
<em>, <a>, <code>,
<span> — are parsed within their parent text block and rendered
with per-character style overrides.
Fold uses a fork of Troika that adds styleRanges support — the ability
to override font weight, font style, and color on arbitrary character ranges within
a single text mesh. This means a paragraph with a bold word, an italic phrase,
and a coloured link is a single draw call, not three separate meshes.
<p>This text has <strong>bold</strong> and <em>italic</em> words rendered natively in 3D — no images, live geometry.</p>
Fold reads computed styles at render time and maps them to Troika text properties. The following CSS properties are supported on text elements:
font-size, font-weight, font-style, font-family
color, text-shadow (multiple layers supported)
line-height, letter-spacing, text-align, text-transform
text-decoration: underline — custom geometry via Troika's getSelectionRects
Fold ships with two TTF font families loaded from /public/fonts/:
Caladea (serif) with regular, bold, italic, and bold-italic variants,
and Lato (sans-serif) with the same four variants. These cover the
majority of font-weight and font-style combinations you'd
use in practice.
Custom fonts can be added by pointing CSS font-family to a TTF file
accessible via your server. Troika loads font files on demand and caches them for
the lifetime of the page — no pre-registration required.
CSS text-shadow declarations are parsed and rendered as additional
Troika text meshes placed behind the primary text mesh. Each shadow layer becomes
a separate draw call with the shadow's offset, blur, and color applied. Multiple
comma-separated shadows are all rendered.
h1 { text-shadow: 0 0 40px rgba(100, 160, 255, 0.4), -1px 0 rgba(240, 218, 67, .5); }
When text-decoration: underline is present — or when an <a>
element inherits underline styling — Fold generates a custom Underlines
component. It calls Troika's getSelectionRects to obtain precise glyph
bounding boxes, then places thin rectangular meshes beneath each underlined character
run. This approach handles word-wrapping and proportional widths correctly without any
CSS approximation.
Clicking or hovering text ranges is supported via raycasting against the bounding
boxes returned by getSelectionRects. Links styled with
data-foldpage-view-link are fully interactive in 3D — the camera flies
to the target view on click, and hover states update correctly even when the text
mesh is rotated in space.
This works because Fold's event manager uses clientX / clientY
coordinates against the canvas, then intersects rays with per-character bounding
quads rather than the text mesh as a whole. A user clicking the second word of a
two-word link activates the link correctly.
view-apex at
rotateX(-38deg) translateY(-1250px) translateX(200px) translateZ(420px).
The camera tilts 38° back to read it fully.
Overflow & Scroll
Elements with overflow: scroll or overflow: auto that
contain enough content to overflow are rendered as stencil-masked scroll
containers in 3D. The outer bounds of the element clip via a WebGL Mask
plane; an inner spring-animated group handles vertical scroll driven by the canvas
wheel event.
<div style="overflow: auto; height: 300px; width: 100%;"> <p>Long content that scrolls independently...</p> <p>...within its clipped 3D region.</p> </div>
Content is clipped at the element's bounds. No scrolling. Stencil mask applied, no scroll group.
Clips and scrolls when scrollHeight > clientHeight. Spring-animated inner group.
Fold assigns each overflow container a stencil ID from a shared pool (IDs 10–254,
wrapping). The outer element renders an OverflowClipMask — a
drei Mask plane at the container's screen bounds that writes its ID
to the WebGL stencil buffer.
All child meshes within that container read the stencil context via React's
OverflowStencilContext and apply useMask props so they
only draw where the stencil matches. The effect is a hard rectangular clip that
works correctly in 3D space — even when the container itself is rotated or
translated.
Scrolling is handled by an OverflowScrollGroup — a spring-based
Y-translation group that listens for wheel events on the canvas.
It initialises from the DOM element's current scrollTop so that
content already scrolled in the DOM appears correctly on first render.
Bidirectional sync keeps the DOM's scrollTop in step with the
3D scroll position.
view-low:
rotateX(22deg) translateY(680px) translateZ(260px).
The camera approached from above, angling slightly downward.
The Pipeline
Fold's core loop runs once on Fold.init() and produces a static
Three.js scene from the current DOM. The pipeline has five stages:
uiConfNodes selectors —
configured tag names and CSS classes mapped to render types (Text, Background, Image, Video).
FoldGroupElement reads each element's getBoundingClientRect()
and scroll offsets, then places a Three.js group at the element's center in world space
using the pixelSizeDefault = 0.0007 scale factor.
TText meshes;
block backgrounds → RoundedPlane; images → ImageMesh;
video → FoldVideo texture plane.
FoldGroup wraps all scene content in a react-spring animated group.
Page scroll, window resize, and XR thumbstick/touch input all drive the spring
target, which eases the group into position each frame.
data-foldpage-view elements uses
getBoundingClientRect() combined with CSS transform extraction to
compute the element's world-space normal, then positions the camera along that
normal at a comfortable viewing distance.
The result is that any page already marked up with semantic HTML can become a
3D spatial experience by adding the foldpage class, a few
data-foldpage-view attributes, and CSS transforms on
.myelement.foldpage-el selectors.
view-deep:
rotateX(38deg) translateY(1250px) translateX(-180px) translateZ(420px).
You are now approximately 875px below the origin in CSS space, the camera tilted 38° down.
Explore the other demos at the demos index or read the Views & Navigation Docs to learn about data attributes, camera controls, WebXR, and the complete API surface.