About
A cylinder of ninety-six segments laid on its edge. Its two faces share one canvas texture drawn at build: a radial metal gradient, fine concentric turning marks, a raised rim and an inner ring, the words set letter by letter around a circle, and the mark with a soft shadow. The edge uses a stripe texture as a bump map for the reeding. The metal reflects the stage's studio, so highlights run across it as it turns. An idle sine turns it about its vertical axis; each hover adds a full turn that the coin eases into, so it always lands face-on.
Use it when
- A loyalty programme, rewards or referral page wants an object for the reward.
- A fintech, savings or crypto-adjacent brand wants a mark in metal.
- An anniversary, membership or thank-you page wants a keepsake.
Skip it when
- The brand cannot be read as money or a token.
- The page already has a 3D centrepiece.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the component with the shadcn CLI:
npx shadcn@latest add @vibebox/coin - 3Use it:
import { Coin } from "@/components/vibebox/coin"; const colors = { metal: "#d7dce6", relief: "#4c5668" }; <Coin className="mx-auto aspect-square w-full max-w-[420px]" mark="A" ring="Member since 2026" colors={colors} />
Code
3 filescomponents/vibebox/coin/coin.client.tsx
/**
* VibeBox Motion · Coin
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
"use client";
import { useCallback } from "react";
import { SculptureCanvas } from "@/lib/vibebox/sculpture-canvas";
import type { CoinOptions } from "./coin.sculpture";
/* Warm, neutral rim lights: the stage's spectrum lights would tint the metal red and green. */
const METAL_LIGHTS = { left: "#fff1d6", right: "#ffe3b0", back: "#ffffff" };
export type CoinProps = CoinOptions & {
/** Accessible name. */
label?: string;
className?: string;
};
/* A flat gold disc with the mark, in CSS, until the 3D coin is ready and wherever WebGL is not. */
function CoinPoster({ mark, metal, relief }: { mark: string; metal: string; relief: string }) {
return (
<span aria-hidden="true" className="absolute inset-0 grid place-items-center">
<span
className="grid aspect-square w-[52%] place-items-center rounded-full font-serif text-[clamp(2rem,9vw,5rem)] font-bold shadow-[0_24px_50px_-20px_rgb(0_0_0/0.6)]"
style={{ background: `radial-gradient(circle at 35% 30%, #fff, ${metal} 24%, ${metal})`, color: relief, boxShadow: `inset 0 0 0 10px ${metal}, inset 0 0 0 14px ${relief}` }}
>
{[...mark].slice(0, 3).join("")}
</span>
</span>
);
}
export function Coin({ label = "A gold coin turning slowly; hold the pointer over it and it turns over", className, ...options }: CoinProps) {
const load = useCallback(() => import("./coin.sculpture").then((module) => module.createCoin), []);
return (
<SculptureCanvas
slug="coin"
load={load}
options={options}
label={label}
poster={<CoinPoster mark={options.mark ?? "V"} metal={options.colors?.metal ?? "#e7b64f"} relief={options.colors?.relief ?? "#9a6a12"} />}
lights={METAL_LIGHTS}
className={className}
/>
);
}components/vibebox/coin/coin.sculpture.ts
/**
* VibeBox Motion · Coin
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import * as THREE from "three";
import { clamp01, damp, disposeObject, easeOutCubic } from "@/lib/vibebox/engine/three";
import type { Sculpture, SculptureFrame } from "@/lib/vibebox/engine/three";
/*
* A struck coin: a short cylinder with a reeded edge and the same die on both faces (a double
* ring of words and a mark in the middle), in a metal that reflects the stage's studio. It
* spins slowly on its edge, wobbling a little; a hold turns it over and brings the face to you.
*/
export type CoinOptions = {
/** One to three characters struck in the middle of the face. */
mark?: string;
/** Words around the rim of the face (repeated to fill the ring). */
ring?: string;
/** The metal and the colour of the striking, as CSS colours. */
colors?: { metal?: string; relief?: string };
};
function dieTexture(mark: string, ring: string, metal: string, relief: string): THREE.CanvasTexture {
const size = 1024;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const c = canvas.getContext("2d")!;
const mid = size / 2;
// A soft hot spot toward the upper left, kept off the mark.
const face = c.createRadialGradient(mid * 0.5, mid * 0.45, 20, mid, mid, mid * 1.15);
face.addColorStop(0, "rgba(255,255,255,0.9)");
face.addColorStop(0.28, metal);
face.addColorStop(1, metal);
c.fillStyle = face;
c.fillRect(0, 0, size, size);
// Fine concentric turning marks, as a die leaves.
c.lineWidth = 1;
for (let r = 30; r < mid; r += 6) {
c.strokeStyle = `rgba(0,0,0,${0.025 + 0.02 * ((r / 6) % 3)})`;
c.beginPath();
c.arc(mid, mid, r, 0, Math.PI * 2);
c.stroke();
}
// The raised rim and the inner ring.
c.strokeStyle = relief;
c.lineWidth = 22;
c.beginPath();
c.arc(mid, mid, mid - 26, 0, Math.PI * 2);
c.stroke();
c.lineWidth = 6;
c.beginPath();
c.arc(mid, mid, mid - 84, 0, Math.PI * 2);
c.stroke();
c.beginPath();
c.arc(mid, mid, mid * 0.56, 0, Math.PI * 2);
c.stroke();
// The words, set around the ring.
const text = (ring.trim() || "VALUE") .toUpperCase();
const ringText = (text + " · ").repeat(Math.max(2, Math.ceil(26 / (text.length + 3)))).slice(0, 36);
c.fillStyle = relief;
c.font = "600 64px ui-sans-serif, system-ui, sans-serif";
c.textAlign = "center";
c.textBaseline = "middle";
const letters = [...ringText];
const radius = mid - 150;
letters.forEach((letter, index) => {
const angle = (index / letters.length) * Math.PI * 2 - Math.PI / 2;
c.save();
c.translate(mid + Math.cos(angle) * radius, mid + Math.sin(angle) * radius);
c.rotate(angle + Math.PI / 2);
c.fillText(letter, 0, 0);
c.restore();
});
// The mark.
const chars = [...mark].slice(0, 3).join("");
c.font = `700 ${chars.length > 1 ? 260 : 360}px ui-serif, Georgia, serif`;
c.shadowColor = "rgba(0,0,0,0.35)";
c.shadowBlur = 14;
c.shadowOffsetY = 6;
c.fillStyle = relief;
c.fillText(chars, mid, mid + 12);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = 8;
return texture;
}
function reedTexture(): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 8;
const c = canvas.getContext("2d")!;
c.fillStyle = "#808080";
c.fillRect(0, 0, 128, 8);
for (let x = 0; x < 128; x += 4) {
c.fillStyle = "#e8e8e8";
c.fillRect(x, 0, 2, 8);
c.fillStyle = "#222";
c.fillRect(x + 2, 0, 2, 8);
}
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.repeat.set(10, 1);
return texture;
}
export function createCoin(options: CoinOptions = {}): Sculpture {
const metal = options.colors?.metal ?? "#e7b64f";
const relief = options.colors?.relief ?? "#9a6a12";
const object = new THREE.Group();
const pivot = new THREE.Group();
object.add(pivot);
const face = dieTexture(options.mark ?? "V", options.ring ?? "Thank you", metal, relief);
const reed = reedTexture();
// The metal also glows a little with its own colour, so it never goes black between reflections.
const glow = new THREE.Color(metal).multiplyScalar(0.2);
const faceMaterial = new THREE.MeshStandardMaterial({ map: face, emissiveMap: face, emissive: glow, metalness: 0.8, roughness: 0.34, envMapIntensity: 1.5 });
const edgeMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(metal), emissive: glow, bumpMap: reed, bumpScale: 1.4, metalness: 0.9, roughness: 0.34, envMapIntensity: 1.5 });
// A cylinder's axis is Y and its caps' images lie turned a quarter: turn the geometry about
// its axis so the die reads upright, then stand it on its edge with the face toward the viewer.
const geometry = new THREE.CylinderGeometry(1, 1, 0.16, 96, 1);
geometry.rotateY(Math.PI / 2);
const coin = new THREE.Mesh(geometry, [edgeMaterial, faceMaterial, faceMaterial]);
coin.rotation.x = Math.PI / 2;
pivot.add(coin);
let spin = 0.5;
let flip = 0;
let flipTarget = 0;
let built = 0;
let wasHover = false;
return {
object,
assembleSeconds: 1.5,
holdSeconds: 30,
assemble(progress) {
built = easeOutCubic(clamp01(progress));
object.position.y = (1 - built) * 2.4;
object.scale.setScalar((0.5 + 0.5 * built) * 1.55);
pivot.rotation.z = (1 - built) * 1.2;
},
update(frame: SculptureFrame) {
if (frame.still) {
pivot.rotation.set(0.12, -0.5, 0);
return;
}
const hovering = frame.hover > 0.35;
// Each time the pointer arrives the coin turns over once.
if (hovering && !wasHover) flipTarget += Math.PI * 2;
wasHover = hovering;
flip += (flipTarget - flip) * damp(4.5, frame.delta);
spin += frame.delta * (hovering ? 0.2 : 0.55);
pivot.rotation.y = Math.sin(spin) * 0.85 + flip;
pivot.rotation.x = 0.1 + Math.sin(frame.time * 0.6) * 0.1;
object.position.y = Math.sin(frame.time * 0.9) * 0.05;
},
dispose() {
disposeObject(object);
face.dispose();
reed.dispose();
faceMaterial.dispose();
edgeMaterial.dispose();
},
};
}components/vibebox/coin/index.tsx
/**
* VibeBox Motion · Coin
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
export { Coin, type CoinProps } from "./coin.client";
export type { CoinOptions } from "./coin.sculpture";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| mark | string | "V" | One to three characters struck in the middle. |
| ring | string | "Thank you" | Words around the rim, repeated to fill the ring. |
| colors | { metal?: string; relief?: string } | { metal: "#e7b64f", relief: "#9a6a12" } | The metal and the colour of the striking. |
| label | string | — | Accessible name. |
| className | string | — | Merged with cn(); give it a size, about square. |
Accessibility and motion
- Keyboard
- Not focusable; the coin is an illustration.
- Screen readers
- An image named by
label. - Touch
- A finger on the coin turns it over; scrolling is never blocked.
- Reduced motion
- One composed pose of the coin, turned a little; it does not spin or flip.
Edge cases and limits
- Keep
markto one to three characters: more is unreadable on the face. - Emoji depend on the visitor's fonts; a plain letter is safer.
- The words repeat to fill the ring: a very long phrase is cut off.
Related

Brochure
FreeA 3D brochure whose paper bends: click a page, drag a corner or press the arrow keys to turn it.
3D · three.js

Dotted Globe
FreeA globe of dots with pulsing markers and arcs; drag to turn it, choose a place to spin to it.
3D · three.js

Gift Box
FreeA wrapped gift whose lid swings open on hover, with a warm light and a burst of confetti.
3D · three.js