Three.js Scroll Hourglass — Volume-True Sand Timer
Three.js Scroll Hourglass Sand Timer · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Driven Hourglass With Conserved Sand Volume in Three.js

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:
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
- 1Load the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
- 2Paste HTML, CSS, and JSAn antique hourglass — wood frame, glass bulbs, full top pile — rotates slowly under warm light with 0% ELAPSED in the HUD.
- 3Scroll 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.
- 4Watch the endgameNear 88% the top pile collapses rapidly — the cube-root law's signature — and the stream dies as the last volume transfers.
- 5Complete the flipThe final 12% of scroll smoothsteps the whole rig through a half-turn, resetting the timer upside down.
- 6Re-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
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().