
The weightless objects
Physics, 3D, Hover
NewFree
Two real CSS 3D dice thrown from a leather cup: they roll across the cloth with real physics, bounce, knock into each other and into the rail and settle on a face. The sum drops in, big, with a bounce.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dice on the table</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the dice night of a made-up board game bar. On the left the name and the roll; on the right a felt table seen
from above, with a walnut rail, two dice and a leather cup. The dice, their faces and their shadows are built
by the script -->
<section class="side">
<header class="brand">
<svg class="brand__mark" viewBox="0 0 40 40" aria-hidden="true">
<rect x="5" y="5" width="30" height="30" rx="8" transform="rotate(-12 20 20)"/>
<circle cx="14.5" cy="15" r="2.6"/><circle cx="20" cy="20" r="2.6"/><circle cx="25.5" cy="25" r="2.6"/>
</svg>
<p class="brand__name">Pip & Felt<span>Board game bar</span></p>
</header>
<div class="score" data-score>
<div class="score__intro" data-intro>
<p class="score__kicker">Thursday dice night</p>
<h1 class="score__title">Roll for the house round</h1>
</div>
<div class="score__result" data-result aria-hidden="true">
<p class="score__kicker" data-name>Natural seven</p>
<p class="score__sum"><span data-sum>7</span></p>
<p class="score__parts" data-parts>4 + 3</p>
</div>
</div>
<p class="sr" role="status" aria-live="polite" data-said></p>
<div class="actions">
<button class="roll" type="button" data-roll data-autoclic>
<span class="roll__fill" aria-hidden="true"></span>
<span class="roll__label" data-roll-label>Roll the dice</span>
</button>
<p class="rule">House rule: doubles buy the next round.</p>
</div>
</section>
<div class="scene" data-scene>
<div class="jolt" data-jolt>
<div class="table" data-table>
<div class="rail" aria-hidden="true"></div>
<div class="felt" aria-hidden="true"><span class="felt__print">Pip & Felt</span></div>
<div class="wall wall--back" aria-hidden="true"></div>
<div class="wall wall--front" aria-hidden="true"></div>
<i class="anchor" data-anchor></i>
<i class="anchor" data-anchor></i>
</div>
</div>
<div class="cup-place" data-place>
<div class="cup" data-cup>
<svg viewBox="0 0 120 150" aria-hidden="true">
<defs>
<linearGradient id="dt-leather" x1="0" x2="1">
<stop offset="0" stop-color="var(--leather-dark)"/>
<stop offset="0.3" stop-color="var(--leather-light)"/>
<stop offset="0.55" stop-color="var(--leather)"/>
<stop offset="1" stop-color="var(--leather-dark)"/>
</linearGradient>
<linearGradient id="dt-band" x1="0" x2="1">
<stop offset="0" stop-color="var(--leather-deep)"/>
<stop offset="0.32" stop-color="var(--leather-dark)"/>
<stop offset="1" stop-color="var(--leather-deep)"/>
</linearGradient>
<radialGradient id="dt-mouth" cx="0.5" cy="0.2" r="0.8">
<stop offset="0" stop-color="#140b06"/>
<stop offset="1" stop-color="#3a2012"/>
</radialGradient>
</defs>
<path class="cup__body" d="M6 18 Q8 90 15 140 Q60 152 105 140 Q112 90 114 18 Z" fill="url(#dt-leather)"/>
<path d="M9 40 Q60 52 111 40 L110.6 52 Q60 64 9.3 52 Z" fill="url(#dt-band)"/>
<path d="M13.5 118 Q60 130 106.5 118 L105.6 130 Q60 142 14.4 130 Z" fill="url(#dt-band)"/>
<path class="cup__stitch" d="M10 45 Q60 57 110 45"/>
<path class="cup__stitch" d="M14.5 123.5 Q60 135.5 105.5 123.5"/>
<path class="cup__seam" d="M88 24 Q89 80 92 138"/>
<path class="cup__stitch" d="M85 26 Q86 80 89 136"/>
<g class="cup__crest">
<rect x="44" y="72" width="26" height="26" rx="6" transform="rotate(-12 57 85)"/>
<circle cx="51.5" cy="80" r="2"/><circle cx="57" cy="85" r="2"/><circle cx="62.5" cy="90" r="2"/>
</g>
<ellipse cx="60" cy="18" rx="54" ry="12" fill="url(#dt-mouth)"/>
<path class="cup__rim" d="M6 18 A54 12 0 0 0 114 18 A54 12 0 0 0 6 18 Z"/>
<path class="cup__shine" d="M24 30 Q26 80 30 128"/>
</svg>
</div>
</div>
<p class="hint" data-hint>Tap the table to throw</p>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.
36 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “Dice on the table” from Animorio and fit it into my siteEach die is a div with transform-style: preserve-3d and six faces pushed out by half a side with translateZ, turned with rotateX and rotateY. The pips sit in a three by three grid, with an inner shadow that makes them look drilled. The rounded corners would leave holes at the edges: inside the die there are three dark ivory planes, one per axis, which fill them and act as the bevel. The die lives inside the table, tilted with rotateX, so for the die up is the table's z axis.
Each die has a position, a speed, an orientation (a quaternion) and a spin. At every step gravity pulls it down; if one of its eight corners goes under the cloth or past the rail, an impulse is born at that corner: along the normal, with the bounce, and sideways, as much as friction allows. Precisely because the impulse is at the corner and not at the centre, the die starts spinning and rolls. A cube has the same inertia round every axis (mass times side squared over six), so the sums stay simple. The two dice hit each other like two balls, and the knock sets them spinning too.
The whole throw is worked out on the click, at 480 steps a second, and sixty a second are kept: the position and quaternion of each die. Then a GSAP tween on a single number runs through the frames, with a slerp between one and the next. So the throw sits in the same timeline as the cup and the result, the speed is a timeScale, and the knocks against the rail (which the sums noted with their moment) become calls that jolt the table at the right time.
A die counts as still when it touches the cloth, is slow and its highest face points almost straight up. In the last twelve frames it is turned by the least it takes to lie flat on that face: a quaternion that brings its highest axis onto the z axis. The number is read from there: the axis pointing up gives the face. After three seconds a growing brake stops the dice anyway, even with a slippery cloth and rubber dice.
On every frame the normal of each face, turned with the die, is compared with the direction of the light: a face that turns away gets a thicker dark veil. The shadow is a radial gradient on the cloth, pushed away from the light and wider and lighter the higher the die is. The cup is a flat SVG drawing over the scene: GSAP moves its foot in table coordinates, two invisible anchors inside the table say where that lands on screen and how big it is there, and GSAP rattles it and tips it over round its lower corner. Its mouth, worked out again in table units, is the point the dice leave from.
| Name | File | Default | What it changes |
|---|---|---|---|
THROW | script.js | 1 | How hard the cup throws the dice: at 0.5 they stop halfway across the table, at 1.6 they crash into the far rail. |
BOUNCE | script.js | 0.42 | How much they bounce on the cloth: 0 they thud down, 0.7 they seem made of rubber. |
FELT | script.js | 0.5 | The grip of the cloth: low the dice slide, high they stop rolling sooner. |
SPIN | script.js | 1 | How much the dice spin as they leave the cup. |
TILT | script.js | 48 | How much from above the table is seen, in degrees: 30 almost from the side, 62 almost from above. |
SPEED | script.js | 1 | The speed of the whole throw: 2 is twice as fast, 0.5 half as fast, a slow motion. |
For a board game bar or shop, a games site, a draw or a random prize in a promotion, the «surprise me» page of a menu, a party game, the wait for a result that has to feel truly left to chance.
The same animation with the sizes adapted: the table becomes deeper than wide, below the heading and above the button, the dice stay big enough to read the pips and the throw goes from the far left towards the near right. A tap on the table or on the button is enough.
Nothing moves: when the page opens the table, the words and the dice fade in. On a click the dice fade away, the throw is still worked out and they reappear already still where the physics left them, with the sum fading in.
GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 15 KB; the libraries: GSAP 28 KB.
Tested on 10 October 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.