Three.js Scroll Hourglass — Volume-True Sand Timer

Three.js Scroll Hourglass Sand Timer · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Cube-root volume conservation: top scales by cbrt(1 − run), bottom by cbrt(run) — sand never evaporates
Emergent drama: the top pile collapses fast at the end purely from the volume law, no easing tricks
Cone tips pinned through scaling by recomputing Y from scaled height each frame
Grain stream with lifetime recycling, gravity-like acceleration, and spawn-gating by run state
Kill-line that tracks the rising bottom pile so grains visibly land on the surface at any fill level
Believable glass from MeshPhysicalMaterial: 16% opacity, near-zero roughness, depthWrite off — no refraction shader
One rig Group so the end-of-scroll flip rotates glass, piles, and stream as a unit
Phase-split scrub: 88% sand run, 12% smoothstepped flip, reversible in exact order

About this UI Snippet

How to Build a Scroll-Driven Hourglass With Conserved Sand Volume in Three.js

Screenshot of the Three.js Scroll Hourglass Sand Timer snippet rendered live

The Three.js Scroll Hourglass Sand Timer snippet runs sand through a glass as the user scrolls — the upper pile shrinking, a thin accelerating grain stream threading the neck, the lower pile growing — and, once the last grain falls, flips the entire instrument upside down in the final 12% of scroll, ready to run again. GSAP's ScrollTrigger scrubs elapsed time; the sand piles obey a cube-root law that keeps their combined volume honest.

Volume conservation is a cube root

The naive approach scales pile height linearly with progress, which loses sand: a cone at half scale holds one-eighth the volume, so linear-scaling piles would visibly evaporate mid-run. Since pile volume scales with the cube of uniform scale, the correct mapping is scale = cbrt(fraction) — the top pile scales by cbrt(1 − run) and the bottom by cbrt(run), making their volumes sum to a constant at every scroll position. The visible consequence is exactly what real hourglasses do: the top pile seems barely to shrink at first, then collapses rapidly at the end, while the bottom pile leaps up early and creeps later. The physics produces the drama for free.

Pinning cone tips through scaling

Scaling a cone happens about its center, which would make the top pile float off the neck as it shrinks. Both piles therefore get their Y position recomputed from their scaled height each frame — the inverted top cone's tip stays pinned at the neck (y = 0.4 + scaledHeight/2), and the bottom pile's base stays welded to the bulb floor. It is the scaling counterpart of the hinge-translation trick in the book pages snippet: decide which edge must not move, then position from that edge.

A grain stream that falls, not drips

The neck stream is 220 recycled particles with wrapping lifetimes, respawning at the neck only while sand is actually running (0 < run < 1) — the same spawn-gating as the rocket launch exhaust, so the stream starts and stops as a consequence of state rather than a toggle. Each grain's fall speed grows with its lifetime (0.18 + life × 0.55), approximating gravitational acceleration; grains vanish when they meet the rising surface of the bottom pile, whose height the kill-line tracks via botSand.position.y + 2.6 × scale. The stream visibly lands *on* the pile at every fill level.

Glass from MeshPhysicalMaterial, frame from primitives

The bulbs are two open-ended cones at 16% opacity with near-zero roughness and depthWrite: false — the standard recipe for believable glass without refraction shaders: transparency plus specular highlight equals glass to the eye, the same budget-material philosophy as the cover glass in the exploded view. The wooden frame is two discs and three pillars of rough MeshStandardMaterial, warm-lit to set the antique mood.

The flip closes the loop

Everything — glass, frame, piles, stream — lives in one rig Group, so the final phase can smoothstep rig.rotation.z through π and the whole instrument turns over as a unit. Because the flip window sits after the sand window (88%/12% of one scrubbed value, the phase-split pattern from the Rubik's cube assembly), scrolling back first un-flips, then un-pours. The elapsed HUD reads from the run fraction — a scroll-progress display in the spirit of a scroll reading-time indicator, but diegetic: the page's progress bar is the sand itself.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to re-derive the volume math. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain the cube-root conservation law, the tip-pinning position formula, or the spawn-gated grain stream. The same assistant can refine the illusion — a slight sand-level slope using a scaled hemisphere instead of a cone tip, dust motes puffing where the stream lands, a second full run after the flip by extending the scrub range and mirroring the pile roles, or binding the run fraction to a real deadline (time remaining until your event) instead of scroll. It can also restyle the instrument — brass frame, colored sand, frosted glass — by adjusting the three materials. Treat the code as a starting point to interrogate and reshape, not a finished artifact.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-driven hourglass sand timer" in plain HTML, CSS, and JavaScript using Three.js and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).

Requirements:
- A pinned full-viewport section with a canvas, WebGLRenderer, PerspectiveCamera (resized with aspect on window resize), and warm ambient + directional light.
- One rig Group containing everything, so an end-of-scroll flip can rotate the whole instrument.
- Glass bulbs: two open-ended ConeGeometry meshes meeting at a neck, MeshPhysicalMaterial at ~16% opacity, roughness ~0.05, DoubleSide, depthWrite false. Wood frame: two CylinderGeometry discs top and bottom plus three pillars.
- Sand piles as cones with VOLUME CONSERVATION: top pile scale = cbrt(1 − run), bottom = cbrt(run), where run = min(1, p / 0.88). Recompute each pile's position.y from its scaled height every frame so the top cone's tip stays pinned at the neck and the bottom cone's base stays on the bulb floor.
- A grain stream of ~220 recycled particles: wrapping lifetimes; on wrap respawn at the neck ONLY while 0.005 < run < 0.995 (else park at y = −9999); fall speed grows with lifetime (gravity feel); despawn when y drops below the bottom pile's apex, computed as botSand.position.y + 2.6 × bottomScale so grains land on the rising surface.
- One GSAP tween (ease "none") scrubbing p 0→1 on a ScrollTrigger with pin: true, scrub ~0.5, end ~+=450%.
- Phase split: sand runs over p ∈ [0, 0.88]; the final 12% smoothsteps rig.rotation.z through π, flipping the instrument for a reset.
- A slow orbiting camera with a clock-driven bob, an ELAPSED % HUD from the run fraction, and an intro overlay fading at p > 0.02.
- Confirm reverse scrolling un-flips first, then un-pours — piles regrowing and the stream reappearing — with no one-shot events.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
  2. 2
    Paste HTML, CSS, and JSAn antique hourglass — wood frame, glass bulbs, full top pile — rotates slowly under warm light with 0% ELAPSED in the HUD.
  3. 3
    Scroll to run the sandA thin accelerating stream threads the neck; the top pile shrinks on the cube-root law while the bottom pile rises to meet the falling grains.
  4. 4
    Watch the endgameNear 88% the top pile collapses rapidly — the cube-root law's signature — and the stream dies as the last volume transfers.
  5. 5
    Complete the flipThe final 12% of scroll smoothsteps the whole rig through a half-turn, resetting the timer upside down.
  6. 6
    Re-dress the instrumentSwap sand and wood colors, bulb opacity, or GRAINS count; the volume math only cares about the run fraction.

Real-world uses

Common Use Cases

Deadline and countdown campaigns
Sales, launches, and event pages get a literal timer whose urgency accelerates visually near the end — stronger than a numeric circular countdown.
Reading-progress storytelling
Pin the hourglass beside an article so the sand tracks scroll like a diegetic scroll reading-time indicator.
Teaching volume-preserving animation
The cube-root law is the star: a memorable one-line lesson in why uniform scale is not linear in quantity.
Heritage and craft brand heroes
Watchmakers, whisky, and slow-craft brands get patience rendered as an object, warmer than the mechanics of a gear train.
Turn-based and strategy game promos
The flip moment is a natural "your move" beat for games marketing turn mechanics.
SHOP
Limited-drop commerce pages
Run the sand across a product story and land the flip on the buy CTA — scarcity you scrub through.

Got questions?

Frequently Asked Questions

A cone scaled uniformly by s holds s³ times its volume, so a pile at half scale contains only 12.5% of its sand. Scaling linearly would make sand vanish mid-run. Using scale = cbrt(fraction), volume is proportional to fraction itself, and top (1 − run) plus bottom (run) sums to constant total volume at every scroll position. The visible bonus: the top pile collapses dramatically near the end, exactly like a real hourglass.

Scaling happens about a mesh's center, so a shrinking cone would drift away from the neck. Each frame both piles get position.y recomputed from their scaled height — the inverted top cone's tip pinned at the neck via y = 0.4 + (height × scale)/2, the bottom cone's base held at the bulb floor. Fix the edge that must not move, derive the center from it.

Respawn gating: when a grain's lifetime wraps, it only re-enters at the neck if 0.005 < run < 0.995; otherwise it parks at y = −9999. In-flight grains finish their fall and die on the pile surface, so the stream drains naturally rather than blinking off — and reappears the same way when scrolling back into the running window.

The kill-line is computed from the same values that scale the pile: a grain despawns when its y drops below botSand.position.y + 2.6 × bottomScale, which tracks the pile apex as it rises. Because pile scale and kill-line derive from one run fraction, the stream always terminates visually on the sand surface, at 5% full or 95%.

Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Build the rig and ScrollTrigger inside a mount effect against a canvas ref; the grain Float32Arrays live in effect scope. On cleanup kill the ScrollTrigger, dispose the glass, wood, sand, and grain geometries/materials (note the bottom pile clones the sand material — dispose both), and call renderer.dispose().