A minimal, zero-dependency coin toss app built with React. Features a real 3D flip animation using an authentic King Charles III £2 silver coin (Royal Mint, 2023). No backend. No API keys. No installs beyond React itself.
Built with pure CSS 3D transforms — no animation libraries, no bloat. One file, self-contained, drop it in and go.
💡 Tip — Self-Contained by Design
Both coin faces (heads and tails) are base64-encoded directly inside
FlipDrop.jsx. No external assets, no broken image paths, no CDN dependencies. The file works anywhere React works.
- 🪙 Real 3D Flip Animation — CSS
rotateYwith perspective gives a true coin-in-air effect - ✅ Always Lands Correctly — result is decided first, then the animation lands on the matching face
- 🎲 Randomised Spin Count — each flip spins a different number of times so it never feels scripted
- 📊 Live Tally — running count of heads, tails, and total flips
- 🔤 DM Mono Typography — clean monospace font throughout; swap to Departure Mono with one line
- 🖤 White Single UI — pure white background, no gradients, no shadows, no noise
- 📦 Zero External Assets — coin images are embedded as base64, fully portable
- ⚡ Single File — entire app lives in one
.jsxfile, no component splitting needed
Requirements: Node.js 16+ · React 18+
npx create-react-app flipdrop
cd flipdrop
cp path/to/FlipDrop.jsx src/FlipDrop.jsxUpdate src/App.js:
import FlipDrop from './FlipDrop';
export default function App() {
return <FlipDrop />;
}Then run:
npm startnpm create vite@latest flipdrop -- --template react
cd flipdrop
npm install
cp path/to/FlipDrop.jsx src/FlipDrop.jsxUpdate src/App.jsx the same way, then:
npm run devimport FlipDrop from './FlipDrop';
// Renders full-page coin toss UI
export default function App() {
return <FlipDrop />;
}- Open the app — the coin displays heads side by default
- Click Flip — the coin launches into a randomised 3D spin
- The coin settles on the correct face (heads or tails)
- The result label fades in below the coin
- The tally updates — heads / total / tails
| What | Where in code | Example |
|---|---|---|
| Coin images | HEADS / TAILS constants |
Swap in any base64 PNG |
| Flip duration | animation: toss 1.5s |
Change 1.5s to taste |
| Coin size | .coin-stage width/height |
Default 220px |
| Font family | @import + font-family |
See Font section below |
| Button style | .btn class |
Border, padding, colours |
| Brand name | .brand in JSX |
Flip<span>Drop</span> |
Convert any PNG to base64 and replace the constants at the top of FlipDrop.jsx:
base64 -w 0 my-heads.png > heads.b64
base64 -w 0 my-tails.png > tails.b64Then paste the output into:
const HEADS = "data:image/png;base64,YOUR_BASE64_HERE";
const TAILS = "data:image/png;base64,YOUR_BASE64_HERE";DM Mono (default) is loaded via Google Fonts. To use Departure Mono locally:
- Download Departure Mono from departuremono.com
- Place the
.woff2file in yourpublic/folder - Replace the
@importline at the top of the styles string:
@font-face {
font-family: 'Departure Mono';
src: url('/DepartureMono-Regular.woff2') format('woff2');
font-weight: 400;
}- Update
font-familyinbody:
body {
font-family: 'Departure Mono', monospace;
}FlipDrop/
├── src/
│ ├── FlipDrop.jsx # Entire app — styles, logic, markup
│ └── App.jsx # Entry point (just renders <FlipDrop />)
├── public/
│ └── index.html # Standard CRA/Vite shell
├── package.json
└── README.md
HEADS/TAILS— Base64-encoded coin face imagesstyles— Full CSS string injected via<style>tagFlipDrop()— Main component; owns all statetoss()— Decides result, calculates end rotation, triggers animation.coin-wrap.spinning— CSS keyframetossdrives the 3D flip.tally— Renders heads / total / tails after the first flip
| Face | Design |
|---|---|
| Heads | King Charles III portrait · effigy by Martin Jennings |
| Tails | Royal Cypher with St Edward's Crown · floral emblems of the four nations |
Denomination: £2 · Royal Mint · 2023
Material: .999 fine silver (bullion coin)
The flip uses CSS custom properties to pass the landing rotation into the keyframe:
.coin-wrap.spinning {
animation: toss 1.5s cubic-bezier(0.33, 0, 0.66, 1) forwards;
}
@keyframes toss {
0% { transform: rotateY(0deg) translateY(0px); }
50% { transform: rotateY(900deg) translateY(-80px); }
100% { transform: rotateY(var(--end)) translateY(0px); }
}- Heads lands at
n × 360°(front face visible) - Tails lands at
n × 360° + 180°(back face visible) backface-visibility: hiddenon each face ensures only one shows at a time
const [spin, setSpin] // Animation running flag
const [result, setResult] // "heads" | "tails" | null
const [show, setShow] // Controls fade-in of result label
const [end, setEnd] // CSS rotation value passed to keyframe
const [counts, setCounts] // { heads: n, tails: n }FlipDrop was designed and built as a clean single-UI React experiment. Coin imagery courtesy of the Royal Mint. Typography by DM Mono.
MIT — see LICENSE for details.
- 🐛 Report issues — GitHub Issues
- 💬 Discussions — GitHub Discussions
© 2025 FlipDrop. All rights reserved.
