wrapType

Text on a surface,
still real DOM.

npm ↗
GitHub ↗
TypeScript·Three.js CSS3DRenderer·React + Vanilla JS

CSS transforms can fake 3D. wrapType does it properly — distributing real DOM text elements across the surface of a sphere, cylinder, torus, or flat plane using Three.js's CSS3DRenderer. Variable fonts, CSS animations, and every other Liiift tool compose naturally because the characters are actual HTML, not canvas pixels.

Interactive demo

Shape
Fill
Size — 144 px
Rotation
Repeat
Character curve — 0%
Text

Drag to orbit. Scroll to zoom. Characters are measured with canvas measureText and justified to fill each row exactly — no fixed tracking. The flag animates each frame with no DOM writes.

How it works

Real DOM on every surface

Three.js's CSS3DRenderer places HTML elements in 3D space using CSS perspective and matrix3d transforms. Each character is its own element, oriented so its face points along the surface normal at that point.

Surface geometry, not a canvas

Character positions are computed analytically from the shape equations — no UV unwrapping or texture atlases. Spheres use latitude-band distribution scaled by circumference; cylinders and tori follow arc-length parameterisation.

Four fill modes

Cover tiles the entire surface. Flow runs a single band around the circumference or equator. Full-width fills the widest pass. Full-height runs pole-to-pole. Text always repeats to fill the chosen region.

Composes with everything

Because characters are real DOM, font-variation-settings, CSS animations, hover states, and other Liiift tools all work without any special integration. The renderer re-mounts the scene when props change.

Custom meshes

Drop any .glb, .gltf, or .obj file onto the demo above. wrapType samples the surface with MeshSurfaceSampler, orients each character along the local normal, then auto-scales to fit the scene radius.

Prefer GLB over OBJ

GLB is a self-contained binary bundle — no separate .mtl or texture files. Normals, materials, and the scene hierarchy are all embedded. OBJ works but ships without normals by default; wrapType computes them automatically when absent.

One mesh, merged geometry

wrapType picks the first Mesh it finds in the scene graph. For multi-part models, merge all objects into a single mesh before exporting so the full surface is sampled, not just one component.

Keep polygon count reasonable

5,000–50,000 triangles is plenty. Higher counts do not improve text placement quality — MeshSurfaceSampler distributes characters area-proportionally regardless. Lighter meshes load and parse faster in the browser.

Scale and coordinate system

GLTF and OBJ both use Y-up, right-handed axes. wrapType auto-fits the bounding box to its radius option, so absolute model scale does not matter. Recalculate normals in your DCC tool after any non-uniform scaling to keep character orientation correct.

Usage

TypeScript + React · Vanilla JS

React component

import { WrapTypeScene } from '@liiift-studio/wraptype'

<WrapTypeScene
  text="Typography is the art and technique of arranging type"
  shape="sphere"
  fill="cover"
  fontSize={13}
  color="rgba(220,210,255,0.85)"
  autoRotate
  style={{ width: '100%', height: '500px' }}
/>

React hook

import { useWrapType } from '@liiift-studio/wraptype'

const { ref } = useWrapType({
  text: 'Typography is the art and technique of arranging type',
  shape: 'cylinder',
  fill: 'flow',
})

<div ref={ref} style={{ width: '100%', height: '500px' }} />

Vanilla JS

import { getCharPositions, createWrapScene } from '@liiift-studio/wraptype'

const container = document.getElementById('scene')
const positions = getCharPositions({
  text: 'Typography is the art and technique of arranging type',
  shape: 'torus',
  fill: 'cover',
})
const scene = createWrapScene(container, positions, { autoRotate: true })

// Later:
scene.destroy()

Options

OptionDefaultDescription
textThe string to distribute across the surface. Repeats to fill.
shape'sphere''sphere' · 'cylinder' · 'torus' · 'plane'
fill'cover''cover' · 'flow' · 'full-width' · 'full-height'
fontSize14Character font size in px.
color'#fff'CSS color applied to every character element.
radius300Surface radius in scene units.
autoRotatefalseContinuously rotate the scene.
autoRotateSpeed1.0Rotation speed multiplier.
camera'orbit''orbit' (drag to rotate, scroll to zoom) · 'fixed'
charAdvanceRatio0.62Fraction of fontSize used as character advance width.
lineHeightRatio1.4Line height multiplier relative to fontSize.